From 449366028b260c8e14e7155a763bf6fd68a59f6e Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 15 Oct 2026 00:47:22 -0400 Subject: [PATCH 1/2] fix(host): name a teammate channel's owner when refusing owner-only commands rename, share, archive, kill, move and the other owner-only commands now resolve through one helper that, when the name isn't this host's, checks the running host's team listing and refuses with " is 's channel on ; only its owner can ". Names nobody lists still say "No channel ". The CLI also takes SIGINT back from the ignored disposition shells give background jobs, so `kill -INT` ends a backgrounded `ace watch`. Why: apps/host/src/cli.ts#function own | owner-only commands act only on this host's catalog; a teammate's channel is refused with its owner and host, never resolved or relayed Why: apps/host/src/cli.ts#function main | every command but serve must install a SIGINT listener, since Bun keeps the SIG_IGN that shells give background jobs --- ...56A2BA0VE9-mode-ace-mode-channels-id-an.md | 5 +- ...WK6JSKPDZG-teammates-channels-go-throug.md | 6 +- ...DQD9MY2W4E-owner-only-commands-act-only.md | 8 +++ ...T78X1E0KAE-every-command-but-serve-must.md | 8 +++ apps/host/src/cli.ts | 59 ++++++++++++++++--- 5 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 .lane/memory/apps/host/src/cli.ts/01M4YY5SXAC7MHCCDQD9MY2W4E-owner-only-commands-act-only.md create mode 100644 .lane/memory/apps/host/src/cli.ts/01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md diff --git a/.lane/memory/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md b/.lane/memory/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md index a2101927..a44c472a 100644 --- a/.lane/memory/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md +++ b/.lane/memory/apps/app/src/composer.tsx/01M4YQG7BVWEFZPD56A2BA0VE9-mode-ace-mode-channels-id-an.md @@ -4,8 +4,9 @@ anchor: function Composer created: 2026-10-15T02:50:44Z norm: '1' sig: 6e0e6c5c8afbd01a -body_hash: 802fd6ff25732d0d -raw_hash: 19e83ce1538d800e +body_hash: f202c0f88ed3fe4c +raw_hash: ddc707e16566aea8 +vouched: 2026-10-15T04:47:31Z lines: 99-282 --- diff --git a/.lane/memory/apps/host/src/cli.ts/01M4YFWABA5884D6WK6JSKPDZG-teammates-channels-go-throug.md b/.lane/memory/apps/host/src/cli.ts/01M4YFWABA5884D6WK6JSKPDZG-teammates-channels-go-throug.md index 72076839..025ba456 100644 --- a/.lane/memory/apps/host/src/cli.ts/01M4YFWABA5884D6WK6JSKPDZG-teammates-channels-go-throug.md +++ b/.lane/memory/apps/host/src/cli.ts/01M4YFWABA5884D6WK6JSKPDZG-teammates-channels-go-throug.md @@ -4,9 +4,9 @@ anchor: function join created: 2026-10-15T00:37:32Z norm: '1' sig: 2e2a490821164beb -body_hash: e578693bef0388bb -raw_hash: 8e0483dfea2e961f -vouched: 2026-10-15T03:39:53Z +body_hash: cfd68c1e1f71ebcd +raw_hash: bd472575a375bbf3 +vouched: 2026-10-15T04:47:29Z lines: 201-223 --- diff --git a/.lane/memory/apps/host/src/cli.ts/01M4YY5SXAC7MHCCDQD9MY2W4E-owner-only-commands-act-only.md b/.lane/memory/apps/host/src/cli.ts/01M4YY5SXAC7MHCCDQD9MY2W4E-owner-only-commands-act-only.md new file mode 100644 index 00000000..e30374e6 --- /dev/null +++ b/.lane/memory/apps/host/src/cli.ts/01M4YY5SXAC7MHCCDQD9MY2W4E-owner-only-commands-act-only.md @@ -0,0 +1,8 @@ +--- +id: 01M4YY5SXAC7MHCCDQD9MY2W4E +anchor: function own +created: 2026-10-15T04:47:23Z +norm: '1' +--- + +owner-only commands act only on this host's catalog; a teammate's channel is refused with its owner and host, never resolved or relayed diff --git a/.lane/memory/apps/host/src/cli.ts/01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md b/.lane/memory/apps/host/src/cli.ts/01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md new file mode 100644 index 00000000..beded8ac --- /dev/null +++ b/.lane/memory/apps/host/src/cli.ts/01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md @@ -0,0 +1,8 @@ +--- +id: 01M4YY5SXJFQK8VXT78X1E0KAE +anchor: function main +created: 2026-10-15T04:47:23Z +norm: '1' +--- + +every command but serve must install a SIGINT listener, since Bun keeps the SIG_IGN that shells give background jobs diff --git a/apps/host/src/cli.ts b/apps/host/src/cli.ts index a822af83..3f507ced 100755 --- a/apps/host/src/cli.ts +++ b/apps/host/src/cli.ts @@ -170,19 +170,43 @@ async function isHostRunning(): Promise { /** What a teammate can do in a channel from the app; everything else acts on this host's own. */ const participant = new Set(["info", "say", "ask", "watch", "chat", "stop"]); +/** What only a channel's owner can do, phrased for refusing a teammate's channel. */ +const owned: Record = { + backup: "back it up", + rename: "rename it", + kill: "kill it", + move: "move it", + share: "change its sharing", + desktop: "change its desktop tools", + archive: "archive it", + unarchive: "unarchive it", + delete: "delete it", +}; + +function mine(ref: string): boolean { + return catalog.list().some((record) => record.id === ref || record.name === ref); +} + +/** The listing `ref` names, by ID before name. */ +function listed(listings: Listing[], ref: string): Listing | undefined { + const identified = listings.filter((listing) => listing.id === ref); + const matches = identified.length ? identified : listings.filter((value) => value.name === ref); + if (matches.length > 1) throw new Error(`More than one channel is named ${ref}; use its ID`); + return matches[0]; +} + /** A channel this host runs, or else one a reachable teammate's host runs. */ async function join(ref: string): Promise<{ name: string; session: Session }> { - if (catalog.list().some((record) => record.id === ref || record.name === ref)) { + if (mine(ref)) { const record = catalog.find(ref); return { name: record.name, session: await Connection.open(record.id) }; } const client = await gateway(); try { - const listings = await loopback.within(client.request({ op: "channels" })); - const identified = listings.filter((listing) => listing.id === ref); - const matches = identified.length ? identified : listings.filter((value) => value.name === ref); - if (matches.length > 1) throw new Error(`More than one channel is named ${ref}; use its ID`); - const listing = matches[0]; + const listing = listed( + await loopback.within(client.request({ op: "channels" })), + ref, + ); if (!listing) throw new Error(`No channel ${ref} here or on a reachable teammate's host`); const session: Session = { request: (request, watch) => client.channel(listing.id, request, watch), @@ -195,6 +219,19 @@ async function join(ref: string): Promise<{ name: string; session: Session }> { } } +/** + * A channel this host runs, for an owner-only command. A teammate's channel the running host can + * see is refused by its owner and host, since info, say, ask and watch accept the same name. + */ +async function own(ref: string, action: string): Promise { + if (mine(ref) || !(await isHostRunning())) return catalog.find(ref); + const listing = listed(await local({ op: "channels" }), ref); + if (!listing) return catalog.find(ref); + throw new Error( + `${listing.name} is ${listing.owner}'s channel on ${listing.host}; only its owner can ${action}`, + ); +} + function printTabs(windows: WindowInfo[]): void { if (flags.json) return console.log(JSON.stringify(windows, null, 2)); if (!windows.length) return console.log("No open channel windows"); @@ -215,6 +252,9 @@ function printTabs(windows: WindowInfo[]): void { async function main() { if (flags.help || !command) return console.log(HELP); + // Shells start background jobs with SIGINT ignored and Bun keeps that, so `kill -INT` would not + // end a backgrounded watch. A listener takes SIGINT back; serve shuts down with its own. + if (command !== "serve") process.on("SIGINT", () => process.exit(130)); if (command !== "serve" && command !== "logs") { openLog("cli"); log("info", "cli.command", { command, ...(ref ? { ref } : {}) }); @@ -365,7 +405,7 @@ async function main() { if (flags.detach) return void (await session.request(body)); await watch(session, false); const { submission } = await session.request<{ submission: number }>(body); - process.on("SIGINT", () => { + process.prependListener("SIGINT", () => { console.log(dim(`\nDetached; the run continues. Stop it with: ace stop ${name}`)); process.exit(130); }); @@ -390,7 +430,9 @@ async function main() { session.close(); } } - const record = catalog.find(ref); + const action = owned[command]; + if (!action) throw new Error(`Unknown command ${command}\n\n${HELP}`); + const record = await own(ref, action); switch (command) { case "backup": if (rest.length !== 1) throw new Error("ace backup "); @@ -437,7 +479,6 @@ async function main() { await remove(record.id); return console.log(`Deleted ${record.name}. Its lane branches remain in ${record.project}.`); } - throw new Error(`Unknown command ${command}\n\n${HELP}`); } main().then( From f2abf40e32b4a60582029c931eb963a15fe61f7b Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 15 Oct 2026 00:47:37 -0400 Subject: [PATCH 2/2] lane: sync owner-only-refusal memory --- ...01M4YY5SXAC7MHCCDQD9MY2W4E-owner-only-commands-act-only.md | 4 ++++ ...01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.lane/memory/apps/host/src/cli.ts/01M4YY5SXAC7MHCCDQD9MY2W4E-owner-only-commands-act-only.md b/.lane/memory/apps/host/src/cli.ts/01M4YY5SXAC7MHCCDQD9MY2W4E-owner-only-commands-act-only.md index e30374e6..07605700 100644 --- a/.lane/memory/apps/host/src/cli.ts/01M4YY5SXAC7MHCCDQD9MY2W4E-owner-only-commands-act-only.md +++ b/.lane/memory/apps/host/src/cli.ts/01M4YY5SXAC7MHCCDQD9MY2W4E-owner-only-commands-act-only.md @@ -3,6 +3,10 @@ id: 01M4YY5SXAC7MHCCDQD9MY2W4E anchor: function own created: 2026-10-15T04:47:23Z norm: '1' +sig: 17c24c18fb873711 +body_hash: dc8cdbeb7e0fe839 +raw_hash: e6a59fb7db2e880a +lines: 226-233 --- owner-only commands act only on this host's catalog; a teammate's channel is refused with its owner and host, never resolved or relayed diff --git a/.lane/memory/apps/host/src/cli.ts/01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md b/.lane/memory/apps/host/src/cli.ts/01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md index beded8ac..284efe67 100644 --- a/.lane/memory/apps/host/src/cli.ts/01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md +++ b/.lane/memory/apps/host/src/cli.ts/01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md @@ -3,6 +3,10 @@ id: 01M4YY5SXJFQK8VXT78X1E0KAE anchor: function main created: 2026-10-15T04:47:23Z norm: '1' +sig: 7d5d3f63a3bddde4 +body_hash: fc2a39d751558e82 +raw_hash: 51dcd6d26a74b487 +lines: 253-482 --- every command but serve must install a SIGINT listener, since Bun keeps the SIG_IGN that shells give background jobs