@fungi.computer/truffle 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/local-board-agents.d.ts +46 -0
  2. package/dist/local-board-agents.js +138 -0
  3. package/dist/local-board-shipping.d.ts +129 -0
  4. package/dist/local-board-shipping.js +325 -0
  5. package/dist/local-extensions.d.ts +2 -0
  6. package/dist/local-extensions.js +121 -36
  7. package/dist/local-login.js +33 -19
  8. package/dist/local-upgrade.d.ts +63 -0
  9. package/dist/local-upgrade.js +358 -0
  10. package/dist/private-local-agent-control.d.ts +8 -1
  11. package/dist/private-local-agent-control.js +16 -2
  12. package/dist/private-local-auth-rpc.d.ts +5 -0
  13. package/dist/private-local-auth-rpc.js +46 -4
  14. package/dist/private-local-client-version.js +3 -1
  15. package/dist/private-local-delegation.d.ts +18 -0
  16. package/dist/private-local-delegation.js +82 -21
  17. package/dist/private-local-exec-review.js +45 -8
  18. package/dist/private-local-extension-archive-notice.d.ts +8 -0
  19. package/dist/private-local-extension-archive-notice.js +108 -0
  20. package/dist/private-local-extension-records.d.ts +25 -0
  21. package/dist/private-local-extension-records.js +51 -0
  22. package/dist/private-local-process-identity.d.ts +14 -0
  23. package/dist/private-local-process-identity.js +51 -0
  24. package/dist/private-local-runtime-host.d.ts +7 -1
  25. package/dist/private-local-runtime-host.js +88 -31
  26. package/dist/private-local-service-contract.d.ts +1 -0
  27. package/dist/private-local-service-discovery.js +2 -3
  28. package/dist/private-local-service-owner.js +80 -9
  29. package/dist/private-local-service-termination.js +12 -11
  30. package/dist/private-local-service.js +19 -1
  31. package/dist/private-local-session-discovery.d.ts +16 -4
  32. package/dist/private-local-session-discovery.js +68 -4
  33. package/dist/private-mcp-form-contract.d.ts +13 -0
  34. package/dist/private-mcp-form-contract.js +6 -1
  35. package/dist/private-mcp-form-session.d.ts +26 -0
  36. package/dist/private-mcp-form-session.js +8 -5
  37. package/dist/private-provider-rejection.d.ts +1 -1
  38. package/dist/private-provider-rejection.js +28 -8
  39. package/dist/private-sqlite-owner.d.ts +7 -1
  40. package/dist/private-sqlite-owner.js +14 -2
  41. package/package.json +32 -16
@@ -1,3 +1,4 @@
1
+ import * as Effect from "effect/Effect";
1
2
  import { AgentArchiveRefused } from "@fungi.computer/shiitake/agent-control";
2
3
  import { agentDelegationRequestKey, createFleetDelegation, delegationProvenance, delegationReplyMeta, delegationSendMeta, } from "@fungi.computer/shiitake/fleet";
3
4
  /** How long one Local delegation grant stays valid while it is admitted. */
@@ -31,7 +32,67 @@ export function createPrivateLocalDelegation(input) {
31
32
  }
32
33
  return agents;
33
34
  }
34
- /** Send with one caller-owned identity; follow-up replies return here. */
35
+ /**
36
+ * Items a child left waiting on this Agent, by the reply command that told
37
+ * it. When the work that carried the reply ends, whatever the Agent neither
38
+ * answered nor escalated goes to a person, so no waiting item is silent.
39
+ */
40
+ const unattended = new Map();
41
+ let poll;
42
+ /** Escalate the items of every reply whose carrying work has settled. */
43
+ async function settleUnattended() {
44
+ const handling = input.unattended;
45
+ if (handling === undefined)
46
+ return;
47
+ for (const [commandId, entry] of unattended) {
48
+ const settlement = await handling
49
+ .readSettlement(entry.sessionId, commandId)
50
+ .catch((cause) => {
51
+ // Not settled as far as anyone can tell; try again on the next poll.
52
+ Effect.runFork(Effect.logWarning("delegation.settlement", cause));
53
+ return undefined;
54
+ });
55
+ if (settlement === undefined)
56
+ continue;
57
+ unattended.delete(commandId);
58
+ for (const itemId of entry.items)
59
+ await handling
60
+ .escalate({ ...entry.child, itemId }, entry.sessionId)
61
+ .catch((cause) => {
62
+ Effect.runFork(Effect.logWarning("delegation.escalate", cause));
63
+ });
64
+ }
65
+ if (unattended.size === 0 && poll !== undefined) {
66
+ clearInterval(poll);
67
+ poll = undefined;
68
+ }
69
+ }
70
+ function watchUnattended(accepted, receiverSessionId, reply) {
71
+ if (input.unattended === undefined ||
72
+ reply.outcome === "undeliverable" ||
73
+ reply.waitingOnYou === undefined)
74
+ return;
75
+ if (receiverSessionId === undefined)
76
+ return;
77
+ unattended.set(accepted.commandId, {
78
+ sessionId: receiverSessionId,
79
+ child: { agentId: reply.agentId, sessionId: reply.sessionId },
80
+ items: reply.waitingOnYou.map(({ itemId }) => itemId),
81
+ });
82
+ poll ??= setInterval(() => {
83
+ void settleUnattended();
84
+ }, 1_000);
85
+ poll.unref();
86
+ }
87
+ /** Escalate what is owed now and stop watching; the Agent is closing. */
88
+ async function close() {
89
+ await settleUnattended();
90
+ unattended.clear();
91
+ if (poll !== undefined)
92
+ clearInterval(poll);
93
+ poll = undefined;
94
+ }
95
+ /** Send with one caller-owned identity; follow-up and steer replies return here. */
35
96
  async function send(task, sourceSessionId, signal, invocation) {
36
97
  if (invocation === undefined)
37
98
  throw new Error("Delegation requires a tool invocation");
@@ -43,19 +104,9 @@ export function createPrivateLocalDelegation(input) {
43
104
  requestId: task.requestId,
44
105
  });
45
106
  signal.throwIfAborted();
46
- if (task.delivery === "steer") {
47
- const opened = await input.directory.open(task.agentId, signal);
48
- try {
49
- return await opened.agent.session(task.sessionId).send(task.text, {
50
- requestId,
51
- delivery: "steer",
52
- });
53
- }
54
- finally {
55
- await opened.release();
56
- }
57
- }
58
- const meta = delegationSendMeta({ id: input.agentId, name: input.agentId }, invocation.meta);
107
+ // A person reads this name where work waits on its sender.
108
+ const sender = agents.find(({ agentId }) => agentId === input.agentId);
109
+ const meta = delegationSendMeta({ id: input.agentId, name: sender?.name ?? input.agentId }, invocation.meta);
59
110
  // The caller's request key owns replay, not the current model program's bytes.
60
111
  const provenance = await delegationProvenance({
61
112
  agentId: input.agentId,
@@ -71,6 +122,7 @@ export function createPrivateLocalDelegation(input) {
71
122
  }, {
72
123
  agentId: task.agentId,
73
124
  sessionId: task.sessionId,
125
+ delivery: task.delivery,
74
126
  text: task.text,
75
127
  requestId,
76
128
  });
@@ -114,15 +166,22 @@ export function createPrivateLocalDelegation(input) {
114
166
  if (mail.to.agentId !== input.agentId && mesh === undefined)
115
167
  throw new Error("Local addressed mail recipient is not authorized");
116
168
  const agents = mesh ?? (await members(new AbortController().signal));
169
+ const sender = mail.from;
117
170
  const delegation = mail.replyTo.length === 0 ? fleet.authenticateReply(mail) : undefined;
118
- const frozenUndeliverable = delegation?.reply.outcome === "undeliverable" &&
119
- delegation.reply.agentId === mail.from.agentId;
120
- if (!agents.some(({ agentId }) => agentId === mail.from.agentId) &&
121
- !frozenUndeliverable)
171
+ const authenticatedUndeliverable = delegation?.reply.outcome === "undeliverable";
172
+ const senderIsMember = sender.kind === "agent" &&
173
+ agents.some(({ agentId }) => agentId === sender.agentId);
174
+ if (!senderIsMember && !authenticatedUndeliverable) {
175
+ if (sender.kind === "system" &&
176
+ mail.replyTo.length === 0 &&
177
+ mail.reply?.outcome !== "accepted")
178
+ throw new Error("Local reply does not match work this Agent sent");
122
179
  throw new Error("Local mail sender is outside this project");
180
+ }
123
181
  if (mail.replyTo.length > 0) {
124
182
  const [destination] = mail.replyTo;
125
183
  if (mail.replyTo.length !== 1 ||
184
+ mail.from.kind !== "agent" ||
126
185
  destination?.agentId !== mail.from.agentId ||
127
186
  destination.sessionId !== mail.from.sessionId)
128
187
  throw new Error("Local delegated work must reply to its sender");
@@ -137,10 +196,12 @@ export function createPrivateLocalDelegation(input) {
137
196
  if (delegation === undefined)
138
197
  throw new Error("Local reply does not match work this Agent sent");
139
198
  const replier = agents.find(({ agentId }) => agentId === delegation.reply.agentId);
140
- if (replier === undefined && !frozenUndeliverable)
199
+ if (replier === undefined && !authenticatedUndeliverable)
141
200
  throw new Error("Local reply sender is outside this project");
142
201
  const meta = delegationReplyMeta(delegation, replier?.name ?? delegation.reply.agentId);
143
- return input.accept({ ...mail, meta }, meta, delegation.reply);
202
+ const accepted = await input.accept({ ...mail, meta }, meta, delegation.reply);
203
+ watchUnattended(accepted, mail.to.sessionId, delegation.reply);
204
+ return accepted;
144
205
  }
145
- return { send, recall, receive, outstanding: fleet.outstanding };
206
+ return { send, recall, receive, close, outstanding: fleet.outstanding };
146
207
  }
@@ -1,4 +1,5 @@
1
1
  import { gitRuleCoverage, rulePrefix } from "./private-local-exec-rules.js";
2
+ import { quote } from "shell-quote";
2
3
  const gitHookGrantSubcommands = new Set([
3
4
  "add",
4
5
  "branch",
@@ -20,29 +21,62 @@ const gitHookGrantSubcommands = new Set([
20
21
  export function localExecEscapeReview(request, cwd, gitGrant) {
21
22
  if (request.escape === undefined)
22
23
  throw new Error("An escape requires a justification");
23
- const command = request.command ??
24
- JSON.stringify([request.executable, ...(request.args ?? [])]);
25
- const prefix = request.executable === undefined ||
26
- (request.executable === "git" && gitGrant === undefined)
24
+ const argv = request.executable === undefined
27
25
  ? undefined
28
- : rulePrefix([request.executable, ...(request.args ?? [])], gitGrant?.configuredRemotes);
26
+ : [request.executable, ...(request.args ?? [])];
27
+ const command = request.command ?? quote(argv ?? []);
28
+ const prefix = argv === undefined || (argv[0] === "git" && gitGrant === undefined)
29
+ ? undefined
30
+ : rulePrefix(argv, gitGrant?.configuredRemotes);
29
31
  const review = {
30
32
  command,
31
33
  cwd,
32
34
  justification: request.escape.justification,
35
+ form: argv === undefined ? "shell" : "direct",
36
+ kind: argv?.[0] === "fungi" && argv[1] === "extension" && argv[2] === "add"
37
+ ? "extension-install"
38
+ : "exec",
33
39
  };
34
- if (prefix !== undefined) {
40
+ if (argv !== undefined)
41
+ review.argv = argv;
42
+ if (prefix !== undefined && review.kind === "exec") {
35
43
  review.prefix = prefix;
36
44
  review.alwaysAllowCoverage =
37
45
  prefix[0] === "git"
38
46
  ? gitRuleCoverage(prefix[1] ?? "")
39
47
  : `Direct argv commands beginning ${JSON.stringify(prefix)} in this project; shell commands still ask.`;
48
+ if (prefix[0] === "git") {
49
+ const hookCapable = gitHookGrantSubcommands.has(prefix[1] ?? "");
50
+ const summary = (hookCapable
51
+ ? "Git hooks in this project can run code as you, and this Agent can add hooks."
52
+ : "This Git command will run outside the sandbox, as you.") +
53
+ (prefix[1] === "push"
54
+ ? " It can push, force-push or delete any branch."
55
+ : "") +
56
+ (["push", "fetch", "pull"].includes(prefix[1] ?? "")
57
+ ? " Remotes can use your SSH keys."
58
+ : "");
59
+ const detail = (hookCapable
60
+ ? "Git runs hook scripts from the folder the project's hooksPath setting names. That setting must already exist, but this Agent can add hook files there. A configured fsmonitor program in this folder may also be writable by this Agent."
61
+ : summary) +
62
+ (prefix[1] === "push"
63
+ ? " A push grant can push all branches, force-update them or delete remote branches."
64
+ : "") +
65
+ (["push", "fetch", "pull"].includes(prefix[1] ?? "")
66
+ ? " Remote URLs can run ssh with your HOME and keys."
67
+ : "");
68
+ review.risk = { summary, detail };
69
+ }
70
+ else {
71
+ const summary = `Every \`${prefix.join(" ")} …\` command will run outside the sandbox, as you.`;
72
+ review.risk = { summary, detail: summary };
73
+ }
40
74
  }
41
75
  if (request.executable === "git") {
42
76
  const hookCapable = gitHookGrantSubcommands.has(request.args?.[0] ?? "");
43
77
  review.warning =
44
78
  (hookCapable
45
- ? "Project hooks can run code as you under this grant. Only the hooksPath setting must pre-exist; this Agent can create missing hook files there. A configured fsmonitor path in the worktree may also be Agent-writable."
79
+ ? `Project hooks can run code as you ${review.prefix === undefined ? "when this command runs" : "under this grant"}. Only the hooksPath setting must pre-exist; this Agent can create missing hook files there. A configured fsmonitor path in the worktree may also be Agent-writable.`
46
80
  : "This command runs as you outside the sandbox.") +
47
81
  (request.args?.[0] === "push"
48
82
  ? " This grant can push all branches, force-update them, or delete remote branches."
@@ -51,7 +85,10 @@ export function localExecEscapeReview(request, cwd, gitGrant) {
51
85
  ? " Remote URLs can invoke ssh with your HOME and keys."
52
86
  : "");
53
87
  }
54
- if (prefix === undefined)
88
+ if (review.kind === "extension-install")
89
+ review.alwaysAllowUnavailableReason =
90
+ "Extension installs require a fresh approval; no reusable rule applies.";
91
+ else if (review.prefix === undefined)
55
92
  review.alwaysAllowUnavailableReason =
56
93
  request.executable === undefined
57
94
  ? "Shell command text cannot match a saved argv rule; Approve once or Deny."
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+ export declare const extensionArchiveNameSchema: z.ZodString;
3
+ export declare function deferExtensionArchiveNotices(home: string): void;
4
+ export declare function extensionArchiveNoticesAreDeferred(home: string): boolean;
5
+ export declare function acquireExtensionOwnerLock(root: string): Promise<() => Promise<void>>;
6
+ export declare function writeExtensionArchiveNotice(home: string, slug: "unversioned", filename: string, names: readonly string[]): Promise<void>;
7
+ export declare function takePendingExtensionArchiveNoticesUnderOwnerLock(home: string): Promise<string[]>;
8
+ export declare function takePendingExtensionArchiveNotices(home: string): Promise<string[]>;
@@ -0,0 +1,108 @@
1
+ import { mkdir, readFile, readdir } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import lockfile from "proper-lockfile";
4
+ import writeFileAtomic from "write-file-atomic";
5
+ import { z } from "zod";
6
+ export const extensionArchiveNameSchema = z
7
+ .string()
8
+ .trim()
9
+ .min(1)
10
+ .max(214)
11
+ .refine((name) => /^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/iu.test(name));
12
+ const noticeSchema = z.strictObject({
13
+ version: z.literal(3),
14
+ slug: z.literal("unversioned"),
15
+ filename: z.string().regex(/^installed(?:-[2-9])?\.json$/u),
16
+ names: z.array(extensionArchiveNameSchema).max(20),
17
+ additionalNameCount: z.number().int().min(0),
18
+ shown: z.boolean(),
19
+ });
20
+ const deferredHomes = new Set();
21
+ function absent(cause) {
22
+ return (cause instanceof Error &&
23
+ "code" in cause &&
24
+ (cause.code === "ENOENT" || cause.code === "ENOTDIR"));
25
+ }
26
+ export function deferExtensionArchiveNotices(home) {
27
+ deferredHomes.add(resolve(home));
28
+ }
29
+ export function extensionArchiveNoticesAreDeferred(home) {
30
+ return deferredHomes.has(resolve(home));
31
+ }
32
+ export async function acquireExtensionOwnerLock(root) {
33
+ await mkdir(root, { recursive: true, mode: 0o700 });
34
+ return lockfile.lock(root, {
35
+ realpath: false,
36
+ lockfilePath: join(root, ".owner.lock"),
37
+ stale: 30_000,
38
+ update: 10_000,
39
+ retries: { retries: 20, factor: 1.2, minTimeout: 25, maxTimeout: 100 },
40
+ });
41
+ }
42
+ function formatNotice(notice) {
43
+ const archivePath = `~/.fungi/archive/${notice.slug}/extensions/${notice.filename}`;
44
+ if (notice.names.length === 0)
45
+ return `Extensions from an earlier version were set aside, but no extension names could be read. Re-add each with: fungi extension add <its source>. Archived registry: ${archivePath}.`;
46
+ const more = notice.additionalNameCount > 0
47
+ ? `, and ${notice.additionalNameCount.toString()} more`
48
+ : "";
49
+ return `Extensions from an earlier version were set aside: ${notice.names.join(", ")}${more}. Re-add each with: fungi extension add <its source>. Archived registry: ${archivePath}.`;
50
+ }
51
+ export async function writeExtensionArchiveNotice(home, slug, filename, names) {
52
+ const notice = noticeSchema.parse({
53
+ version: 3,
54
+ slug,
55
+ filename,
56
+ names: names.slice(0, 20),
57
+ additionalNameCount: Math.max(0, names.length - 20),
58
+ shown: false,
59
+ });
60
+ const directory = join(resolve(home), ".fungi", "archive", slug, "extensions");
61
+ await writeFileAtomic(join(directory, `${filename}.notice.json`), `${JSON.stringify(notice)}\n`, { encoding: "utf8", fsync: true, mode: 0o600 });
62
+ }
63
+ export async function takePendingExtensionArchiveNoticesUnderOwnerLock(home) {
64
+ const directory = join(resolve(home), ".fungi", "archive", "unversioned", "extensions");
65
+ let names;
66
+ try {
67
+ names = await readdir(directory);
68
+ }
69
+ catch (cause) {
70
+ if (absent(cause))
71
+ return [];
72
+ throw cause;
73
+ }
74
+ const notices = [];
75
+ for (const name of names
76
+ .filter((candidate) => /^installed(?:-[2-9])?\.json\.notice\.json$/u.test(candidate))
77
+ .sort()) {
78
+ const path = join(directory, name);
79
+ let value;
80
+ try {
81
+ value = JSON.parse(await readFile(path, "utf8"));
82
+ }
83
+ catch (cause) {
84
+ if (absent(cause) || cause instanceof SyntaxError)
85
+ continue;
86
+ throw cause;
87
+ }
88
+ const parsed = noticeSchema.safeParse(value);
89
+ if (!parsed.success || parsed.data.shown)
90
+ continue;
91
+ const archivedName = name.replace(/\.notice\.json$/u, "");
92
+ if (parsed.data.filename !== archivedName)
93
+ continue;
94
+ await writeFileAtomic(path, `${JSON.stringify({ ...parsed.data, shown: true })}\n`, { encoding: "utf8", fsync: true, mode: 0o600 });
95
+ notices.push(formatNotice(parsed.data));
96
+ }
97
+ return notices;
98
+ }
99
+ export async function takePendingExtensionArchiveNotices(home) {
100
+ const root = join(resolve(home), ".fungi", "extensions");
101
+ const release = await acquireExtensionOwnerLock(root);
102
+ try {
103
+ return await takePendingExtensionArchiveNoticesUnderOwnerLock(home);
104
+ }
105
+ finally {
106
+ await release();
107
+ }
108
+ }
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ /** One vocabulary for installed declarations and the activation reader. */
3
+ export declare const capabilities: readonly ["state", "attention", "settled", "send", "timers", "commands", "projection", "run_tools", "files", "prompt", "compaction_handoff"];
4
+ export declare const activationsSchema: z.ZodObject<{
5
+ projects: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>;
6
+ agents: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
7
+ revision: z.ZodString;
8
+ capabilities: z.ZodArray<z.ZodEnum<{
9
+ state: "state";
10
+ attention: "attention";
11
+ settled: "settled";
12
+ send: "send";
13
+ timers: "timers";
14
+ commands: "commands";
15
+ projection: "projection";
16
+ run_tools: "run_tools";
17
+ files: "files";
18
+ prompt: "prompt";
19
+ compaction_handoff: "compaction_handoff";
20
+ }>>;
21
+ skills: z.ZodBoolean;
22
+ }, z.core.$strict>>>;
23
+ }, z.core.$strict>;
24
+ /** Only refused activations belong to the home upgrader, not installed.json. */
25
+ export declare function incompatibleLocalExtensionActivations(home: string): Promise<readonly string[]>;
@@ -0,0 +1,51 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { z } from "zod";
4
+ /** One vocabulary for installed declarations and the activation reader. */
5
+ export const capabilities = [
6
+ "state",
7
+ "attention",
8
+ "settled",
9
+ "send",
10
+ "timers",
11
+ "commands",
12
+ "projection",
13
+ "run_tools",
14
+ "files",
15
+ "prompt",
16
+ "compaction_handoff",
17
+ ];
18
+ export const activationsSchema = z.strictObject({
19
+ projects: z.record(z.string(), z.record(z.string(), z.string())),
20
+ agents: z.record(z.string(), z.record(z.string(), z.strictObject({
21
+ revision: z.string(),
22
+ capabilities: z.array(z.enum(capabilities)),
23
+ /** The opt-in to the revision's skills folder; separate from tools. */
24
+ skills: z.boolean(),
25
+ }))),
26
+ });
27
+ /** Only refused activations belong to the home upgrader, not installed.json. */
28
+ export async function incompatibleLocalExtensionActivations(home) {
29
+ const path = join(resolve(home), ".fungi", "extensions", "activations.json");
30
+ let text;
31
+ try {
32
+ text = await readFile(path, "utf8");
33
+ }
34
+ catch (cause) {
35
+ if (cause instanceof Error &&
36
+ "code" in cause &&
37
+ (cause.code === "ENOENT" || cause.code === "ENOTDIR"))
38
+ return [];
39
+ throw cause;
40
+ }
41
+ let value;
42
+ try {
43
+ value = JSON.parse(text);
44
+ }
45
+ catch (cause) {
46
+ if (cause instanceof SyntaxError)
47
+ return [path];
48
+ throw cause;
49
+ }
50
+ return activationsSchema.safeParse(value).success ? [] : [path];
51
+ }
@@ -1,3 +1,11 @@
1
+ export type ServiceProcessIdentity = Readonly<{
2
+ kind: "ours";
3
+ evidence: "start-time" | "command-line";
4
+ }> | Readonly<{
5
+ kind: "other";
6
+ }> | Readonly<{
7
+ kind: "gone";
8
+ }>;
1
9
  /**
2
10
  * A process-start discriminator. `unverifiable` means no sufficiently precise
3
11
  * reader is implemented on this platform.
@@ -16,5 +24,11 @@ export type ProcessStart = Readonly<{
16
24
  * accepted as signal authority. No sub-second macOS reader is implemented.
17
25
  */
18
26
  export declare function readProcessStart(pid: number): Promise<ProcessStart>;
27
+ /** Never signal a PID merely because a lease names it. */
28
+ export declare function identifyLocalServiceProcess(claim: Readonly<{
29
+ pid: number;
30
+ startedAt: string;
31
+ directory: string;
32
+ }>): Promise<ServiceProcessIdentity>;
19
33
  /** The owner's start discriminator; an unverifiable marker never permits signalling. */
20
34
  export declare function readOwnProcessStart(): Promise<string>;
@@ -1,6 +1,25 @@
1
+ import { execFile } from "node:child_process";
1
2
  import { readFile } from "node:fs/promises";
3
+ import { promisify } from "node:util";
2
4
  import { z } from "zod";
3
5
  const procStartSchema = z.string().regex(/^\d+$/u);
6
+ const execFileAsync = promisify(execFile);
7
+ /** Signal 0 checks existence, not identity; EPERM is still a live process. */
8
+ function processIsGone(pid) {
9
+ try {
10
+ process.kill(pid, 0);
11
+ return false;
12
+ }
13
+ catch (cause) {
14
+ if (cause instanceof Error && "code" in cause) {
15
+ if (cause.code === "ESRCH")
16
+ return true;
17
+ if (cause.code === "EPERM")
18
+ return false;
19
+ }
20
+ throw cause;
21
+ }
22
+ }
4
23
  /**
5
24
  * Linux reads proc(5)'s `starttime` (field 22 of `/proc/<pid>/stat`, in clock
6
25
  * ticks since boot). macOS's ps lstart is only second-resolution and is not
@@ -29,6 +48,38 @@ export async function readProcessStart(pid) {
29
48
  }
30
49
  return { kind: "unverifiable" };
31
50
  }
51
+ /** Never signal a PID merely because a lease names it. */
52
+ export async function identifyLocalServiceProcess(claim) {
53
+ const start = await readProcessStart(claim.pid);
54
+ if (start.kind === "gone")
55
+ return { kind: "gone" };
56
+ if (start.kind === "running")
57
+ return start.start === claim.startedAt
58
+ ? { kind: "ours", evidence: "start-time" }
59
+ : { kind: "other" };
60
+ if (process.platform !== "darwin")
61
+ return { kind: "other" };
62
+ if (processIsGone(claim.pid))
63
+ return { kind: "gone" };
64
+ // ps is only a corroborating channel after the caller has checked the
65
+ // matching service.json and owner.sqlite lease. Its output is bounded; an
66
+ // unreadable or different argv never grants signal authority.
67
+ let argv;
68
+ try {
69
+ ({ stdout: argv } = await execFileAsync("ps", ["-p", String(claim.pid), "-o", "args="], { timeout: 2_000, maxBuffer: 4_096 }));
70
+ }
71
+ catch {
72
+ return processIsGone(claim.pid) ? { kind: "gone" } : { kind: "other" };
73
+ }
74
+ const marker = `--internal-service ${claim.directory}`;
75
+ const offset = argv.indexOf(marker);
76
+ return offset >= 0 &&
77
+ (offset === 0 || /\s/u.test(argv[offset - 1] ?? "")) &&
78
+ (offset + marker.length === argv.trimEnd().length ||
79
+ /\s/u.test(argv[offset + marker.length] ?? ""))
80
+ ? { kind: "ours", evidence: "command-line" }
81
+ : { kind: "other" };
82
+ }
32
83
  /** The owner's start discriminator; an unverifiable marker never permits signalling. */
33
84
  export async function readOwnProcessStart() {
34
85
  const own = await readProcessStart(process.pid);
@@ -80,6 +80,8 @@ export type PrivateLocalRuntimeHostOptions = Readonly<{
80
80
  name?: string | undefined;
81
81
  /** The Agent that created this one; recorded once with its identity. Absent when a human made it. */
82
82
  createdBy?: string | undefined;
83
+ /** The Session whose create request is durably bound to this Local receipt. */
84
+ createdBySessionId?: string | undefined;
83
85
  /** Immutable project-relative folder authority, inherited by child Agents. */
84
86
  folder?: string | undefined;
85
87
  /** Persist before constructing the Agent, which can recover durable work. */
@@ -172,8 +174,12 @@ export type PrivateLocalCreateStore = Readonly<{
172
174
  agentId: LocalStoreId;
173
175
  databasePath: string;
174
176
  }>;
177
+ export type PrivateLocalCreateCreator = Readonly<{
178
+ agentId: string;
179
+ sessionId: string;
180
+ }>;
175
181
  /** Resolve an Agent create request through its durable Local receipt. */
176
- export declare function resolvePrivateLocalCreateStore(root: string, requestId: string): Promise<PrivateLocalCreateStore>;
182
+ export declare function resolvePrivateLocalCreateStore(root: string, requestId: string, creator?: PrivateLocalCreateCreator): Promise<PrivateLocalCreateStore>;
177
183
  /** Build the fixed source read by Shiitake once for each local run. */
178
184
  export declare function createPrivateLocalInstructionSource(port: Awaited<ReturnType<typeof createPrivateInvocationFilesPort>>): InstructionSource;
179
185
  export declare function createPrivateLocalModelSource(models: Models, providerId: string | undefined, modelId: string | undefined, providerCacheKey: string, selectedOwnerReasoning?: ThinkingLevel, defaults?: Readonly<{