Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
b9804a0
feat(extensions): bind trusted external process-event adapters
M00NLIG7 Aug 27, 2026
d8bc637
no-mistakes(review): Enforce owner and remote-home conformance
M00NLIG7 Aug 27, 2026
c7e4de5
no-mistakes(review): Enforce serialized remote extension package life…
M00NLIG7 Aug 27, 2026
9029610
no-mistakes(review): Enforce identity-conditional extension retirement
M00NLIG7 Aug 27, 2026
af7a24f
no-mistakes(review): Serialize extension retirement and recover crash…
M00NLIG7 Aug 27, 2026
738bb61
no-mistakes(review): Unify retirement worker and lifecycle lock owner…
M00NLIG7 Aug 27, 2026
6418b92
no-mistakes(review): Harden extension lifecycle retirement serialization
M00NLIG7 Aug 27, 2026
0ed2845
no-mistakes(review): Unify extension registration and overridden-stat…
M00NLIG7 Aug 27, 2026
7a6183b
no-mistakes(document): Clarify built-in-only captain answer routing
M00NLIG7 Aug 27, 2026
b7963d8
no-mistakes(lint): Captain: fix extension binding ShellCheck findings
M00NLIG7 Aug 27, 2026
82db735
no-mistakes: apply CI fixes
M00NLIG7 Aug 27, 2026
13d759e
no-mistakes: apply CI fixes
M00NLIG7 Aug 27, 2026
9e7792c
no-mistakes: apply CI fixes
M00NLIG7 Aug 27, 2026
af553ab
no-mistakes: apply CI fixes
M00NLIG7 Aug 27, 2026
69c736b
no-mistakes: apply CI fixes
M00NLIG7 Aug 27, 2026
e65abe0
no-mistakes: apply CI fixes
M00NLIG7 Aug 27, 2026
3c8a8df
no-mistakes: apply CI fixes
M00NLIG7 Aug 27, 2026
ba766ff
no-mistakes(review): Use isolated UID mapping for owner conformance
M00NLIG7 Aug 27, 2026
ee266ba
no-mistakes(review): Captain: remove forbidden CI ownership wrapper
M00NLIG7 Aug 28, 2026
12ae732
no-mistakes(review): Serialize extension binding publication
M00NLIG7 Aug 28, 2026
61dfdeb
no-mistakes(review): Document ordinary CI owner-fixture exclusion
M00NLIG7 Aug 28, 2026
4fd53ef
no-mistakes(review): Quarantine orphaned handshake descendants
M00NLIG7 Aug 28, 2026
a391896
no-mistakes(test): Fix orphan attribution
M00NLIG7 Aug 28, 2026
fe387fb
no-mistakes(test): Harden process tracker baseline
M00NLIG7 Aug 28, 2026
b6bc8bc
no-mistakes(test): Harden detached descendant attribution
M00NLIG7 Aug 28, 2026
6991d1d
no-mistakes(test): Use exact invocation-group cleanup
M00NLIG7 Aug 28, 2026
63d8d0a
no-mistakes(test): Bound remote conformance transport crossings
M00NLIG7 Aug 28, 2026
a67c405
no-mistakes(test): Parallelize isolated extension conformance tests
M00NLIG7 Aug 28, 2026
3d7711f
no-mistakes(test): Lifecycle suite still exceeds deadline
M00NLIG7 Aug 28, 2026
1f5f5bc
feat(extensions): bind trusted external process-event adapters
M00NLIG7 Aug 27, 2026
6be642d
no-mistakes(review): Enforce owner and remote-home conformance
M00NLIG7 Aug 27, 2026
d6f3a66
no-mistakes(review): Enforce serialized remote extension package life…
M00NLIG7 Aug 27, 2026
c7b5bb9
no-mistakes(review): Enforce identity-conditional extension retirement
M00NLIG7 Aug 27, 2026
fd9358a
no-mistakes(review): Serialize extension retirement and recover crash…
M00NLIG7 Aug 27, 2026
e2038d6
no-mistakes(review): Unify retirement worker and lifecycle lock owner…
M00NLIG7 Aug 27, 2026
13ccc06
no-mistakes(review): Harden extension lifecycle retirement serialization
M00NLIG7 Aug 27, 2026
03f5eda
no-mistakes(review): Unify extension registration and overridden-stat…
M00NLIG7 Aug 27, 2026
d3f3243
no-mistakes(document): Clarify built-in-only captain answer routing
M00NLIG7 Aug 27, 2026
1c120e3
no-mistakes(lint): Captain: fix extension binding ShellCheck findings
M00NLIG7 Aug 27, 2026
fe3130b
no-mistakes: apply CI fixes
M00NLIG7 Aug 27, 2026
d818c0b
no-mistakes: apply CI fixes
M00NLIG7 Aug 27, 2026
bd26729
no-mistakes: apply CI fixes
M00NLIG7 Aug 27, 2026
c0dcf5e
no-mistakes: apply CI fixes
M00NLIG7 Aug 27, 2026
26964bf
no-mistakes: apply CI fixes
M00NLIG7 Aug 27, 2026
23544b4
no-mistakes: apply CI fixes
M00NLIG7 Aug 27, 2026
0d4f496
no-mistakes: apply CI fixes
M00NLIG7 Aug 27, 2026
ceb06b0
no-mistakes(review): Use isolated UID mapping for owner conformance
M00NLIG7 Aug 27, 2026
19c3051
no-mistakes(review): Captain: remove forbidden CI ownership wrapper
M00NLIG7 Aug 28, 2026
7dced48
no-mistakes(review): Serialize extension binding publication
M00NLIG7 Aug 28, 2026
c112bcf
no-mistakes(review): Document ordinary CI owner-fixture exclusion
M00NLIG7 Aug 28, 2026
7b19340
no-mistakes(review): Quarantine orphaned handshake descendants
M00NLIG7 Aug 28, 2026
ee3e7a4
no-mistakes(test): Fix orphan attribution
M00NLIG7 Aug 28, 2026
a384b58
no-mistakes(test): Harden process tracker baseline
M00NLIG7 Aug 28, 2026
4b304f5
no-mistakes(test): Harden detached descendant attribution
M00NLIG7 Aug 28, 2026
eb2b62b
no-mistakes(test): Use exact invocation-group cleanup
M00NLIG7 Aug 28, 2026
4fe1456
no-mistakes(test): Bound remote conformance transport crossings
M00NLIG7 Aug 28, 2026
14c2611
no-mistakes(test): Parallelize isolated extension conformance tests
M00NLIG7 Aug 28, 2026
91592f0
no-mistakes(test): Lifecycle suite still exceeds deadline
M00NLIG7 Aug 28, 2026
6229d2e
no-mistakes(review): Split extension conformance and forward remote t…
M00NLIG7 Aug 28, 2026
84e454c
no-mistakes(review): Forward malformed remote payloads through fm-on
M00NLIG7 Aug 28, 2026
c304c56
no-mistakes(review): Bound extension coordinator failure cleanup
M00NLIG7 Aug 28, 2026
1491aca
no-mistakes(test): Skip repeated orphan sweep in coordinator children
M00NLIG7 Aug 28, 2026
d6824a1
no-mistakes(test): Queue isolated extension sections through bounded …
M00NLIG7 Aug 28, 2026
e530fee
no-mistakes(test): Bound extension coordinator lane cleanup
M00NLIG7 Aug 28, 2026
3d3fcd9
no-mistakes(test): Split remote lifecycle coordinator sections
M00NLIG7 Aug 28, 2026
8d7b9a7
no-mistakes(test): Coordinator probes pass; aggregate deadline remains
M00NLIG7 Aug 28, 2026
8717e9c
no-mistakes(test): Launch extension sections concurrently
M00NLIG7 Aug 28, 2026
d1be180
no-mistakes(test): Fix coordinator marker publication
M00NLIG7 Aug 28, 2026
def8095
chore: preserve recovered validation ancestry
M00NLIG7 Aug 28, 2026
77a0493
no-mistakes(test): Stabilize extension binding coordinator timing
M00NLIG7 Aug 28, 2026
8ce354f
no-mistakes(lint): Fix extension binding ShellCheck warnings
M00NLIG7 Aug 28, 2026
bbee120
fix(extensions): prove invocation cleanup before retirement
M00NLIG7 Aug 29, 2026
884925f
chore: integrate current upstream before validation
M00NLIG7 Aug 29, 2026
8933a9f
no-mistakes(review): Harden process-event inbox confinement
M00NLIG7 Aug 29, 2026
e7947d1
no-mistakes(review): Preserve legacy capture parity
M00NLIG7 Aug 29, 2026
3d293be
no-mistakes(review): Protect external registry staging
M00NLIG7 Aug 29, 2026
64d066d
no-mistakes(test): Stabilize bounded extension conformance aggregate
M00NLIG7 Aug 29, 2026
128276d
no-mistakes(document): Document external evidence confinement
M00NLIG7 Aug 29, 2026
b3ffa97
no-mistakes(ci): CI phase fixed. The failure was a flaky fixture in `…
M00NLIG7 Aug 29, 2026
03e2a7f
no-mistakes(review): Harden extension staging and lifecycle reservation
M00NLIG7 Aug 29, 2026
3bec57e
no-mistakes(review): Harden external staging and lifecycle reservations
M00NLIG7 Aug 29, 2026
11f0b5e
no-mistakes(review): Wire capture helper into remote conformance
M00NLIG7 Aug 29, 2026
8c3c061
no-mistakes(review): Pin external capture handoff and signal failures
M00NLIG7 Aug 29, 2026
464aac2
no-mistakes(review): Bind pinned capture authority to inherited descr…
M00NLIG7 Aug 29, 2026
e8c035a
no-mistakes(review): Harden descriptor-bound capture authority
M00NLIG7 Aug 29, 2026
0aa9d67
no-mistakes(review): Harden core capture reservation authority
M00NLIG7 Aug 29, 2026
4e180d3
no-mistakes(review): Harden capture reservation boundaries
M00NLIG7 Aug 29, 2026
8011a36
no-mistakes(review): Harden capture reservations and cleanup
M00NLIG7 Aug 29, 2026
8ee7fd5
no-mistakes(review): Harden capture handoff and reservation cleanup
M00NLIG7 Aug 29, 2026
d2f5954
no-mistakes(review): Bind capture handoff to claim descriptors
M00NLIG7 Aug 29, 2026
a43d55f
no-mistakes(review): Release lifecycle locks after host crashes
M00NLIG7 Aug 29, 2026
52ac45f
no-mistakes(review): Pin reservation recovery to recorded state roots
M00NLIG7 Aug 29, 2026
dd9ed6e
no-mistakes(review): Reject control bytes in claim state roots
M00NLIG7 Aug 29, 2026
3ebb1e2
no-mistakes(test): Stabilize extension capture descriptor handoff
M00NLIG7 Aug 29, 2026
c75b2c6
no-mistakes(document): Document extension capture authority boundary
M00NLIG7 Aug 29, 2026
f80665e
no-mistakes(lint): Fix ShellCheck extension binding warnings
M00NLIG7 Aug 29, 2026
f3e8e27
no-mistakes(ci): CI phase result: fixed `bin/fm-procevent.sh` by init…
M00NLIG7 Aug 29, 2026
d32ac7d
no-mistakes(document): Correct extension namespace creation timing
M00NLIG7 Aug 29, 2026
1f1cde1
no-mistakes(lint): Initialize capture locals for ShellCheck
M00NLIG7 Aug 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .agents/skills/process-event-sources/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ bin/fm-captain-hold.sh bind <source-id>
```

The runner then passes each captured result to that source's own adapter `answers` command and pipes the keyed answers it prints into the one keyed-answer intake, which owns every rule about what they mean; the keys are captain-held task ids.
This is generic: any adapter with an `answers` command works, and the runner still wakes you to act on the result.
This is generic across built-in adapters with an `answers` command, and the runner still wakes you to act on the result.
External process-event bindings intentionally expose no answer operation and cannot feed the captain-answer intake.
`captain-hold-lifecycle` owns when a binding is required and what the keys must be.

A configured remote secondmate reply source is armed and handled through `bin/fm-procevent-remote-reply.sh`.
Expand All @@ -58,6 +59,10 @@ When in doubt, arm only the condition half as an ordinary check and keep the act

`bin/fm-procevent.sh --help`, `bin/fm-procevent-lavish.sh --help`, `bin/fm-procevent-when.sh --help`, and `bin/fm-procevent-remote-reply.sh --help` own the exact commands and flags.

An explicitly enabled external adapter registers through `bin/fm-procevent.sh register-extension`, never through a package-discovered script or package-supplied argv.
[`docs/configuration.md`](../../../docs/configuration.md#trusted-external-process-event-adapters-configextensionsd) owns setup and [`docs/extension-bindings.md`](../../../docs/extension-bindings.md) owns the narrow trusted-code and untrusted-evidence boundary.
Use the owner-matched retirement command registration prints, so an older package generation cannot retire its replacement.

Two rules the commands cannot enforce for you:

- **Never run the source's blocking command yourself in a conversational turn.** That is the problem the runner exists to remove, and for a destructive source it also consumes the result where nothing durable can capture it.
Expand All @@ -81,7 +86,7 @@ Two rules the commands cannot enforce for you:
bin/fm-procevent.sh handled <source-id> <sequence>
```
This call is atomically deduplicated by the exact source and sequence: it prints `handled: <id> <seq>` only the first time and `already-handled: <id> <seq>` on every repeat, so a paired effect gated on that distinction is never authorized twice. Reading the event line or the result file is not handling - only this call durably retires the wake, so call it every time, including on a repeat wake for a sequence you already acted on.
: Ask the adapter what the result means rather than parsing it yourself - for Lavish, `bin/fm-procevent-lavish.sh classify <result-file>` returns `feedback`, `ended`, `waiting`, `missing`, or `unknown`. A `feedback` result can still be the last one a review ever produces, so never assume another wake is coming just because the state is not `ended`.
: Ask the adapter what the result means rather than parsing it yourself. `bin/fm-procevent.sh classify <result-file>` routes through the immutable built-in or extension identity captured with that result; for Lavish, its existing direct command returns `feedback`, `ended`, `waiting`, `missing`, or `unknown`. A `feedback` result can still be the last one a review ever produces, so never assume another wake is coming just because the state is not `ended`.
: A routine no-op an adapter positively identifies never becomes a wake at all - it is recorded as handled and stays silent, so you never see it. For Lavish that is exactly an ended session carrying nothing: a board the captain closed without saying anything. A board close carrying a real answer, and every other result, still wakes you unchanged. Never read the absence of a wake as proof a review is still open; ask the source, not the queue.
: A Lavish wake whose source id matches `bin/fm-procevent-lavish.sh source-id "$(bin/fm-bearings-board.sh path)"` is a bearings board result; load the `bearings` skill's board-wake handling regardless of which answer kinds the result contains.
: A `when` wake carries the watch's one terminal captured outcome and may be re-announced until handled: `bin/fm-procevent-when.sh classify <result-file>` returns `fired` (relay the success and its output); `action-failed` (relay the captured error and decide recovery); `condition-error`, `never-true`, or `rejected` (the watch stopped safely without acting - report why and decide whether to re-arm); or `ambiguous` (the action was claimed but its outcome was never captured - verify its effect manually before anything else). Every `when` outcome is terminal and the action is never retried automatically, so after handling and the generic acknowledgement above, run `bin/fm-procevent-when.sh retire <name>` to clean the watch's private records before any re-arm.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@ Firstmate's skills live in two separate places with different audiences:
## Documentation

- [docs/architecture.md](docs/architecture.md) - maintainer architecture for the crew, supervision, worktrees, secondmates, and project modes.
- [docs/configuration.md](docs/configuration.md) - environment variables, `FM_HOME`, runtime backend selection, optional Relay and its X and Discord setup steps, the files you set, and harness support.
- [docs/configuration.md](docs/configuration.md) - environment variables, `FM_HOME`, runtime backend selection, optional Relay and its X and Discord setup steps, trusted external process-event adapter setup, the files you set, and harness support.
- [docs/extension-bindings.md](docs/extension-bindings.md) - maintainer architecture for the narrow trusted external `process-event-adapter/1` package, binding, handshake, and evidence boundary.
- [docs/remote-secondmates.md](docs/remote-secondmates.md) - current setup, routing, transfer, recovery, and safety behavior for whole-home remote second mates.
- [docs/calm.md](docs/calm.md) - current Pi `/calm` behavior and supported presentation limits.
- [docs/voice-relay.md](docs/voice-relay.md) - the optional spoken interface: setup on both machines, measured round-trip cost, what a spoken answer may read, and what this build does not do yet.
Expand Down
129 changes: 129 additions & 0 deletions bin/fm-extension-launch-barrier.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env node
// Static core-owned launch barrier for one trusted extension invocation.
//
// The host starts this file directly with shell=false in a new process group.
// The barrier publishes that exact group identity before it accepts a one-shot
// host release, then starts the already-validated package executable in the
// same group with inherited bounded protocol pipes. It never evaluates source
// text and never discovers package code or authority on its own.

import { spawn } from "node:child_process";
import { open, readFile, rename } from "node:fs/promises";
import path from "node:path";

const READY_SCHEMA = "firstmate.extension-invocation-ready.v1";
const OWNER_SCHEMA = "firstmate.extension-invocation-owner.v1";
const RELEASE_SCHEMA = "firstmate.extension-invocation-release.v1";
const STARTUP_WAIT_MS = 5000;
const MAX_CONTROL_BYTES = 16384;
const POLL_MS = 20;

function die(message) {
process.stderr.write(`extension launch barrier: ${message}\n`);
process.exit(125);
}

function exactKeys(value, expected) {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const actual = Object.keys(value).sort();
const wanted = [...expected].sort();
return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
}

async function readControl(file) {
const bytes = await readFile(file);
if (bytes.length === 0 || bytes.length > MAX_CONTROL_BYTES) die("control record size is invalid");
let value;
try {
value = JSON.parse(bytes.toString("utf8"));
} catch {
die("control record is invalid JSON");
}
return value;
}

async function writeExclusive(file, value) {
const temporary = `${file}.tmp`;
const handle = await open(temporary, "wx", 0o600).catch(() => die("cannot publish launch readiness"));
try {
await handle.writeFile(`${JSON.stringify(value)}\n`, "utf8");
} finally {
await handle.close();
}
await rename(temporary, file).catch(() => die("cannot publish launch readiness"));
}

function sleep(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}

function pidAlive(pid) {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}

async function main() {
const [token, ownerFile, readyFile, releaseFile, hostPidRaw, entrypoint, cwd, verb, ...extra] = process.argv.slice(2);
if (extra.length || !ownerFile || !readyFile || !releaseFile || !token || !hostPidRaw || !entrypoint || !cwd || !verb) {
die("invalid launch arguments");
}
if (![ownerFile, readyFile, releaseFile, entrypoint, cwd].every(path.isAbsolute)) die("launch paths must be absolute");
if (!/^[0-9]+$/u.test(hostPidRaw)) die("host pid is invalid");
const hostPid = Number(hostPidRaw);
if (!Number.isSafeInteger(hostPid) || hostPid <= 1) die("host pid is invalid");
// The host creates this tracked child with detached=true, making its PID the
// invocation PGID before this static file runs. The unguessable token also
// remains in the barrier's exact argv so recovery can reject PID reuse.
const identity = `barrier-token:${token}`;
await writeExclusive(readyFile, {
schema: READY_SCHEMA,
token,
group_pid: process.pid,
group_identity: identity,
});

const deadline = Date.now() + STARTUP_WAIT_MS;
let release;
while (Date.now() < deadline) {
if (!pidAlive(hostPid)) process.exit(125);
try {
release = await readControl(releaseFile);
break;
} catch (error) {
if (error && error.code !== "ENOENT") throw error;
}
await sleep(POLL_MS);
}
if (!release) die("host did not release the launch barrier");
if (!exactKeys(release, ["schema", "token"]) || release.schema !== RELEASE_SCHEMA || release.token !== token) {
die("launch release identity is invalid");
}
const owner = await readControl(ownerFile);
if (!exactKeys(owner, [
"schema", "token", "phase", "host_pid", "host_identity", "group_pid", "group_identity",
"extension_id", "binding_digest", "request_id", "source_id", "operation",
]) || owner.schema !== OWNER_SCHEMA || owner.token !== token || owner.phase !== "group"
|| owner.host_pid !== hostPid || owner.group_pid !== process.pid || owner.group_identity !== identity) {
die("launch ownership was not published before release");
}

const child = spawn(entrypoint, [verb], {
cwd,
env: process.env,
shell: false,
detached: false,
stdio: ["inherit", "inherit", "inherit"],
});
const outcome = await new Promise((resolve) => {
child.once("error", () => resolve({ code: 125, signal: null }));
child.once("close", (code, signal) => resolve({ code, signal }));
});
if (outcome.signal) process.exit(128);
process.exit(outcome.code ?? 125);
}

main().catch((error) => die(error instanceof Error ? error.message : "unexpected launch failure"));
Loading
Loading