Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function main
created: 2026-10-15T04:47:23Z
norm: '1'
sig: 7d5d3f63a3bddde4
body_hash: 5b0f98c8033bcdda
raw_hash: ec46efe56f9ba131
vouched: 2026-10-23T12:46:13Z
body_hash: e00a53c3d7fb5b0a
raw_hash: 3411b5935d9686e3
vouched: 2026-10-23T13:57:30Z
lines: 253-482
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function main
created: 2026-10-22T14:31:16Z
norm: '1'
sig: 7d5d3f63a3bddde4
body_hash: 5b0f98c8033bcdda
raw_hash: ec46efe56f9ba131
vouched: 2026-10-23T12:46:13Z
body_hash: e00a53c3d7fb5b0a
raw_hash: 3411b5935d9686e3
vouched: 2026-10-23T13:57:30Z
lines: 332-614
---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5MGTVCTXQJ8KR8JCAKZ7QEV
anchor: function main
created: 2026-10-23T13:57:30Z
norm: '1'
sig: 7d5d3f63a3bddde4
body_hash: e00a53c3d7fb5b0a
raw_hash: 3411b5935d9686e3
lines: 415-830
---

ace ask rewrites the channel's refusal to name the owner by GitHub login from the running host's team-roster; the channel itself only knows the Tailscale login
33 changes: 24 additions & 9 deletions apps/host/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,11 @@ async function isHostRunning(): Promise<boolean> {
return (await health(Number(flags.port || config.port)))?.home === config.home;
}

/** The running host knows each owner's GitHub login, as the app shows it. */
async function roster(): Promise<Member[]> {
return await isHostRunning() ? await local<Member[]>({ op: "team-roster" }) : [];
}

/** What a teammate can do in a channel from the app; everything else acts on this host's own. */
const SWITCH = new Set(["on", "off"]);

Expand Down Expand Up @@ -547,6 +552,7 @@ async function main() {
}
case "directory": {
const url = ref === "off" ? null : ref;
const rejoined = !!url && directory.url() === directory.canonical(url);
const running = await isHostRunning();
if (ref && running) await local({ op: "directory-set", url });
else if (ref) await directory.configure(url || undefined);
Expand All @@ -558,11 +564,14 @@ async function main() {
}
const { directory: status, tailscale } = await local<Diagnostics>({ op: "diagnostics" });
if (!status.url) return console.log("No directory");
console.log(url ? `Joined ${status.url}` : status.url);
console.log(
rejoined ? `Already joined ${status.url}` : url ? `Joined ${status.url}` : status.url,
);
if (tailscale.loopback) {
return console.log(dim("This host runs with --loopback, so it doesn't sync it."));
}
if (status.serving) console.log("This Mac serves it.");
else if (status.server) console.log(`${status.server} serves it.`);
if (status.error) {
return console.log(`Last sync failed ${ago(status.failed!)}: ${status.error}`);
}
Expand Down Expand Up @@ -635,16 +644,13 @@ async function main() {
case "info": {
// docs/terms.md names it agent access, after `ace agents`; `shared` is the wire name.
const { shared, owner, ...info } = await session.request<ChannelInfo>({ op: "info" });
// The running host knows each owner's GitHub login, as the app shows it.
const roster = await isHostRunning()
? await local<Member[]>({ op: "team-roster" })
: [];
const self = roster.find((member) => member.self);
const members = await roster();
const self = members.find((member) => member.self);
return console.log(JSON.stringify(
{
...info,
host: host || self?.host,
owner: roster.find((member) => member.login === owner)?.github || owner,
owner: members.find((member) => member.login === owner)?.github || owner,
agents: shared,
},
null,
Expand All @@ -664,14 +670,23 @@ async function main() {
text,
model: flags.model ? model(flags.model) : undefined,
} as const;
// The channel refuses by the owner's Tailscale login; name them as the app does.
const refused = async (error: Error): Promise<never> => {
const { shared, owner } = await session.request<ChannelInfo>({ op: "info" });
if (shared || owner === catalog.user) throw error;
const github = (await roster()).find((member) => member.login === owner)?.github;
throw github ? new Error(error.message.replace(owner, github)) : error;
};
if (flags.detach) {
await session.request(body);
await session.request(body).catch(refused);
return console.log(
`Asked ${name}'s agent; the run continues. Follow it with: ace watch ${name}`,
);
}
await watch(session, false);
const { submission } = await session.request<{ submission: number }>(body);
const { submission } = await session.request<{ submission: number }>(body).catch(
refused,
);
process.prependListener("SIGINT", () => {
console.log(dim(`\nDetached; the run continues. Stop it with: ace stop ${name}`));
process.exit(130);
Expand Down
4 changes: 3 additions & 1 deletion apps/host/src/directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,10 @@ export async function adopt(value: string): Promise<void> {
/** Lifecycle events applied while a read is in flight; that read may predate them. */
let reading: Lifecycle<Listing>[] | undefined;

/** `server` is the teammate's Mac serving a tailnet directory, by its tailnet name. */
export function status() {
return { url: url(), synced, error, failed };
const base = url();
return { url: base, server: base && server(base)?.name, synced, error, failed };
}

/** Sync now, or join the sync under way; nothing without a running host. */
Expand Down
2 changes: 2 additions & 0 deletions apps/host/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export type Diagnostics = {
*/
directory: {
url?: string;
/** The Mac serving a tailnet directory, by its tailnet name; unset for a deployed one. */
server?: string;
synced?: number;
error?: string;
failed?: number;
Expand Down