@fungi.computer/oyster 0.4.0 → 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.
- package/dist/agents.d.ts +2 -1
- package/dist/agents.js +63 -8
- package/dist/app-builds.d.ts +1 -1
- package/dist/app-builds.js +16 -6
- package/dist/apps/gate.d.ts +4 -0
- package/dist/apps/gate.js +3 -0
- package/dist/boot-capability.d.ts +2 -1
- package/dist/boot-capability.js +7 -5
- package/dist/boot-host-script.js +1 -1
- package/dist/build/build-jobs.d.ts +3 -3
- package/dist/build/build-jobs.js +10 -5
- package/dist/build/candidate-cli.js +5 -3
- package/dist/build/candidate-cli.mjs +238 -5
- package/dist/build/candidate-diagnostic.d.ts +2 -0
- package/dist/build/candidate-diagnostic.js +16 -0
- package/dist/channels/binding.d.ts +63 -0
- package/dist/channels/binding.js +85 -0
- package/dist/channels/conversations.d.ts +235 -0
- package/dist/channels/conversations.js +705 -0
- package/dist/channels/ids.d.ts +8 -0
- package/dist/channels/ids.js +15 -0
- package/dist/host.d.ts +1 -1
- package/dist/host.js +21 -4
- package/dist/keys.d.ts +4 -1
- package/dist/keys.js +15 -4
- package/dist/machine-link/authority.d.ts +7 -0
- package/dist/machine-link/delivery.d.ts +12 -0
- package/dist/machine-link/delivery.js +22 -1
- package/dist/machine-link/disposition.d.ts +16 -0
- package/dist/machine-link/disposition.js +65 -0
- package/dist/machine-link/host.d.ts +11 -2
- package/dist/machine-link/host.js +125 -5
- package/dist/machine-link/link.d.ts +4 -0
- package/dist/machine-link/link.js +47 -1
- package/dist/page-script.js +4 -4
- package/dist/standalone/agent-mail.js +4 -1
- package/dist/standalone/agent-model-choice.d.ts +4 -1
- package/dist/standalone/agents.d.ts +8 -0
- package/dist/standalone/agents.js +21 -2
- package/dist/standalone/app-set.d.ts +8 -1
- package/dist/standalone/app-set.js +46 -31
- package/dist/standalone/channel-session-door.d.ts +4 -0
- package/dist/standalone/channel-session-door.js +42 -0
- package/dist/standalone/install-apps-bin.d.ts +2 -0
- package/dist/standalone/install-apps-bin.js +3 -0
- package/dist/standalone/install-apps-cli.d.ts +3 -1
- package/dist/standalone/install-apps-cli.js +27 -17
- package/dist/standalone/machine-link.d.ts +2 -0
- package/dist/standalone/machine-link.js +34 -1
- package/dist/standalone/registry.d.ts +1 -1
- package/dist/testing/capabilities.d.ts +2 -0
- package/dist/testing/capabilities.js +3 -1
- package/dist/workers-git.js +1 -1
- package/dist/workers.d.ts +3 -0
- package/dist/workers.js +9 -1
- package/docs/api/agents.md +8 -4
- package/docs/api/apps.md +8 -4
- package/docs/api/browser.md +40 -16
- package/docs/api/keys.md +25 -3
- package/docs/api/workers.md +21 -14
- package/docs/site/agent-quickstart.mdx +23 -0
- package/examples/standalone/wrangler.jsonc +3 -4
- package/package.json +29 -17
|
@@ -23,6 +23,7 @@ export function agentMail(options) {
|
|
|
23
23
|
if (tick === undefined)
|
|
24
24
|
throw new Error("Agent Post lane is not open");
|
|
25
25
|
await tick();
|
|
26
|
+
await mail.stageUndeliverableReplies();
|
|
26
27
|
};
|
|
27
28
|
const mail = new AgentMail({ ...options, post: runtime, drain });
|
|
28
29
|
const host = {
|
|
@@ -57,6 +58,8 @@ export function agentMail(options) {
|
|
|
57
58
|
};
|
|
58
59
|
return {
|
|
59
60
|
host,
|
|
60
|
-
deliver: (value) =>
|
|
61
|
+
deliver: (value) => {
|
|
62
|
+
return mail.send(value);
|
|
63
|
+
},
|
|
61
64
|
};
|
|
62
65
|
}
|
|
@@ -20,7 +20,10 @@ export declare class AgentModelRefused extends Error {
|
|
|
20
20
|
/** Read the maintained Pi catalog; never accept a model from a different egress. */
|
|
21
21
|
export declare function selectAgentModel(choice: AgentModelChoice): Promise<Model<Api>>;
|
|
22
22
|
/** The Agent DO is the only durable owner. Absence is a new-format default, not an old-env reader. */
|
|
23
|
-
export declare function agentModelChoice(storage:
|
|
23
|
+
export declare function agentModelChoice(storage: Readonly<{
|
|
24
|
+
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
25
|
+
put(key: string, value: AgentModelChoice): Promise<void>;
|
|
26
|
+
}>, admit: (choice: AgentModelChoice) => void): {
|
|
24
27
|
read(): Promise<AgentModelChoice>;
|
|
25
28
|
set(input: AgentModelChoice): Promise<AgentModelChoice>;
|
|
26
29
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type StandaloneTenancyEnv } from "./tenancy.js";
|
|
2
2
|
import type { PostEnvelope } from "@fungi.computer/carrier/post";
|
|
3
|
+
import { type ChannelConversations } from "../channels/conversations.js";
|
|
4
|
+
import type { ConversationBinding } from "../channels/binding.js";
|
|
3
5
|
import { type StandaloneMachineLinkBindings, type StandaloneMachineJoinResult } from "./machine-link.js";
|
|
4
6
|
import { type MachineAgentSettlementPrincipal, type MachineAgentHome, type MachineEnrollment, type MachineLinkCaller, type MachineLinkIdentity } from "../machine-link/index.js";
|
|
5
7
|
/** Workers AI binding surface used by the standalone provider adapter. */
|
|
@@ -78,6 +80,12 @@ export declare abstract class StandaloneAgentBase<Env extends StandaloneAgentEnv
|
|
|
78
80
|
#private;
|
|
79
81
|
/** The deployable root's host for this Agent's pinned tenant. */
|
|
80
82
|
protected abstract hostFor(scope: TenantScope): Promise<OysterHost>;
|
|
83
|
+
/**
|
|
84
|
+
* Compose Oyster's channel owner on this Agent's real store and Shiitake.
|
|
85
|
+
* A host must supply its own binding lookup after authenticating its edge;
|
|
86
|
+
* this does not install an inbound route or mint provider credentials.
|
|
87
|
+
*/
|
|
88
|
+
protected channelConversations(binding: (bindingId: string) => ConversationBinding | null): ChannelConversations;
|
|
81
89
|
private uiOwner;
|
|
82
90
|
private machineClient;
|
|
83
91
|
private computerSnapshot;
|
|
@@ -7,6 +7,8 @@ import { AgentCreateRefused, } from "@fungi.computer/shiitake/agent-control";
|
|
|
7
7
|
import { AgentDelegation, AgentDisclosureRefused, machineAgentCodecs, } from "@fungi.computer/shiitake/delegation";
|
|
8
8
|
import { FungiMessageMetaSchema } from "@fungi.computer/agar/agents";
|
|
9
9
|
import { agentMail } from "./agent-mail.js";
|
|
10
|
+
import { createChannelConversations, } from "../channels/conversations.js";
|
|
11
|
+
import { channelSessionDoor } from "./channel-session-door.js";
|
|
10
12
|
import { createStandaloneMachineEnrollment, standaloneMachineAgentHome, standaloneMachineLinkAuthority, } from "./machine-link.js";
|
|
11
13
|
import { createMachineLinkFences, MachineEnrollmentRefused, MachineLinkRefused, createMachineAgentClient, } from "../machine-link/index.js";
|
|
12
14
|
import { memberInboxItemSchema } from "@fungi.computer/puffball/inbox";
|
|
@@ -228,6 +230,22 @@ const instructions = [
|
|
|
228
230
|
].join("\n");
|
|
229
231
|
/** One agent: Shiitake's Cloudflare host over this Durable Object; its root supplies `hostFor`. */
|
|
230
232
|
export class StandaloneAgentBase extends ShiitakeDO {
|
|
233
|
+
/**
|
|
234
|
+
* Compose Oyster's channel owner on this Agent's real store and Shiitake.
|
|
235
|
+
* A host must supply its own binding lookup after authenticating its edge;
|
|
236
|
+
* this does not install an inbound route or mint provider credentials.
|
|
237
|
+
*/
|
|
238
|
+
channelConversations(binding) {
|
|
239
|
+
return createChannelConversations({
|
|
240
|
+
sql: this.ctx.storage.sql,
|
|
241
|
+
transactionSync: (body) => this.ctx.storage.transactionSync(body),
|
|
242
|
+
binding,
|
|
243
|
+
door: channelSessionDoor(() => this.shiitake(), () => {
|
|
244
|
+
// Standalone Agents have no Hub Team grants to deliver.
|
|
245
|
+
}),
|
|
246
|
+
now: () => Date.now(),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
231
249
|
/** Each installed Release's verified catalog surface, built once per Release for this Agent's lifetime. */
|
|
232
250
|
#appSurfaces;
|
|
233
251
|
uiOwner;
|
|
@@ -479,10 +497,11 @@ export class StandaloneAgentBase extends ShiitakeDO {
|
|
|
479
497
|
throw new Error("Delegated reply identity is unavailable");
|
|
480
498
|
if (mail.reply.outcome === "undeliverable")
|
|
481
499
|
return undefined;
|
|
482
|
-
if (mail.from.
|
|
483
|
-
throw new Error("Delegated reply
|
|
500
|
+
if (mail.from.kind !== "agent")
|
|
501
|
+
throw new Error("Delegated reply Agent identity is unavailable");
|
|
484
502
|
return (await this.machineClient()).settlement(mail.from.agentId, mail.from.sessionId, mail.reply.commandId, mail.to.agentId);
|
|
485
503
|
},
|
|
504
|
+
onUndeliverable: (original) => this.delegation().localUndeliverable(original),
|
|
486
505
|
wake: () => this.wakeRuntime(),
|
|
487
506
|
}));
|
|
488
507
|
}
|
|
@@ -60,6 +60,13 @@ export type InstalledStarterApp = Readonly<{
|
|
|
60
60
|
release: Schema.Json;
|
|
61
61
|
timings: StarterAppTimings;
|
|
62
62
|
}>;
|
|
63
|
+
export type StarterAppSetResult = Readonly<{
|
|
64
|
+
installed: readonly InstalledStarterApp[];
|
|
65
|
+
failed: readonly Readonly<{
|
|
66
|
+
installationId: string;
|
|
67
|
+
reason: string;
|
|
68
|
+
}>[];
|
|
69
|
+
}>;
|
|
63
70
|
/**
|
|
64
71
|
* Builds the installer runs at once by default. A build's compiler may take a
|
|
65
72
|
* 4 GB heap, so two fit the platform's 12 GiB Computer (standard-4) with room
|
|
@@ -91,4 +98,4 @@ export declare function installStandaloneAppSet(options: Readonly<{
|
|
|
91
98
|
/** The idle wait between build status reads; the real timer by default. */
|
|
92
99
|
idle?: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
93
100
|
waitMs?: number;
|
|
94
|
-
}>): Promise<
|
|
101
|
+
}>): Promise<StarterAppSetResult>;
|
|
@@ -72,7 +72,13 @@ const publishedSchema = Schema.Struct({
|
|
|
72
72
|
});
|
|
73
73
|
const buildSchema = Schema.Struct({
|
|
74
74
|
jobId: Schema.String,
|
|
75
|
-
status: Schema.Literals([
|
|
75
|
+
status: Schema.Literals([
|
|
76
|
+
"running",
|
|
77
|
+
"succeeded",
|
|
78
|
+
"failed",
|
|
79
|
+
"interrupted",
|
|
80
|
+
"revoked",
|
|
81
|
+
]),
|
|
76
82
|
detail: Schema.NullOr(Schema.String),
|
|
77
83
|
/** Installed exactly as the host encoded it. */
|
|
78
84
|
release: Schema.NullOr(Schema.Json),
|
|
@@ -271,10 +277,15 @@ export async function installStandaloneAppSet(options) {
|
|
|
271
277
|
}
|
|
272
278
|
};
|
|
273
279
|
const installed = new Map();
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
280
|
+
const failures = new Map();
|
|
281
|
+
const fail = (app, cause) => {
|
|
282
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
283
|
+
failures.set(app.installationId, reason);
|
|
284
|
+
options.report(`Failed ${app.installationId}: ${reason}`);
|
|
285
|
+
};
|
|
286
|
+
const reportFailure = ({ app, cause, }) => {
|
|
287
|
+
fail(app, cause);
|
|
288
|
+
};
|
|
278
289
|
const seededQueue = [];
|
|
279
290
|
const waiting = [];
|
|
280
291
|
const seeding = { finished: false };
|
|
@@ -318,21 +329,19 @@ export async function installStandaloneAppSet(options) {
|
|
|
318
329
|
const seedAll = async () => {
|
|
319
330
|
try {
|
|
320
331
|
for (const app of standaloneAppSet) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
332
|
+
try {
|
|
333
|
+
const seeded = await seedApp(app);
|
|
334
|
+
const waiter = waiting.shift();
|
|
335
|
+
if (waiter === undefined)
|
|
336
|
+
seededQueue.push(seeded);
|
|
337
|
+
else
|
|
338
|
+
waiter(seeded);
|
|
339
|
+
}
|
|
340
|
+
catch (cause) {
|
|
341
|
+
reportFailure({ app, cause });
|
|
342
|
+
}
|
|
329
343
|
}
|
|
330
344
|
}
|
|
331
|
-
catch (cause) {
|
|
332
|
-
stop.requested = true;
|
|
333
|
-
failures.push(cause);
|
|
334
|
-
throw cause;
|
|
335
|
-
}
|
|
336
345
|
finally {
|
|
337
346
|
seeding.finished = true;
|
|
338
347
|
for (const waiter of waiting.splice(0))
|
|
@@ -341,7 +350,7 @@ export async function installStandaloneAppSet(options) {
|
|
|
341
350
|
};
|
|
342
351
|
// `concurrency` builds at once, each installing as its build succeeds.
|
|
343
352
|
const slot = async () => {
|
|
344
|
-
for (let next = await takeSeeded(); next !== undefined
|
|
353
|
+
for (let next = await takeSeeded(); next !== undefined; next = await takeSeeded()) {
|
|
345
354
|
const name = next.app.installationId;
|
|
346
355
|
try {
|
|
347
356
|
const buildStarted = performance.now();
|
|
@@ -416,22 +425,28 @@ export async function installStandaloneAppSet(options) {
|
|
|
416
425
|
});
|
|
417
426
|
}
|
|
418
427
|
catch (cause) {
|
|
419
|
-
|
|
420
|
-
failures.push(cause);
|
|
421
|
-
throw cause;
|
|
428
|
+
reportFailure({ app: next.app, cause });
|
|
422
429
|
}
|
|
423
430
|
}
|
|
424
431
|
};
|
|
425
|
-
// Wait for every seed and build to settle before
|
|
426
|
-
|
|
427
|
-
await Promise.allSettled([
|
|
432
|
+
// Wait for every seed and build to settle before returning the full outcome.
|
|
433
|
+
const settled = await Promise.allSettled([
|
|
428
434
|
seedAll(),
|
|
429
435
|
...Array.from({ length: concurrency }, slot),
|
|
430
436
|
]);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
+
for (const outcome of settled)
|
|
438
|
+
if (outcome.status === "rejected")
|
|
439
|
+
throw outcome.reason;
|
|
440
|
+
return {
|
|
441
|
+
installed: standaloneAppSet.flatMap((app) => {
|
|
442
|
+
const result = installed.get(app.installationId);
|
|
443
|
+
return result === undefined ? [] : [result];
|
|
444
|
+
}),
|
|
445
|
+
failed: standaloneAppSet.flatMap((app) => {
|
|
446
|
+
const reason = failures.get(app.installationId);
|
|
447
|
+
return reason === undefined
|
|
448
|
+
? []
|
|
449
|
+
: [{ installationId: app.installationId, reason }];
|
|
450
|
+
}),
|
|
451
|
+
};
|
|
437
452
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ShiitakeOwnerAgent } from "@fungi.computer/shiitake/cloudflare";
|
|
2
|
+
import type { SessionDoor } from "../channels/conversations.js";
|
|
3
|
+
/** Translate a standalone Agent's Shiitake owner into Oyster's channel Session port. */
|
|
4
|
+
export declare function channelSessionDoor(open: () => Promise<ShiitakeOwnerAgent>, deliverGrants: (sessionId: string) => void): SessionDoor;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { authorMeta } from "@fungi.computer/agar/agents";
|
|
2
|
+
import { ShiitakeError } from "@fungi.computer/shiitake";
|
|
3
|
+
/** Translate a standalone Agent's Shiitake owner into Oyster's channel Session port. */
|
|
4
|
+
export function channelSessionDoor(open, deliverGrants) {
|
|
5
|
+
return {
|
|
6
|
+
createSession: async (requestId) => (await open()).createSession(requestId),
|
|
7
|
+
deliverGrants,
|
|
8
|
+
withdraw: async (sessionId, requestId) => (await open()).session(sessionId).withdraw(requestId),
|
|
9
|
+
latestPending: async (sessionId, visitor) => (await open())
|
|
10
|
+
.withMeta(authorMeta(visitor))
|
|
11
|
+
.session(sessionId)
|
|
12
|
+
.latestPending(),
|
|
13
|
+
prompt: async (sessionId, { requestId, text, meta }) => {
|
|
14
|
+
// The conversation row exists now, so its channel admission is known;
|
|
15
|
+
// deliver Agent tools before the first run can call one.
|
|
16
|
+
deliverGrants(sessionId);
|
|
17
|
+
const agent = await open();
|
|
18
|
+
return await agent.withMeta(meta).session(sessionId).command({
|
|
19
|
+
type: "prompt",
|
|
20
|
+
requestId,
|
|
21
|
+
text,
|
|
22
|
+
delivery: "followUp",
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
line: async (sessionId, input) => {
|
|
26
|
+
const agent = await open();
|
|
27
|
+
const meta = input.author === "operator" ? authorMeta(input.identity) : input.meta;
|
|
28
|
+
await agent.withMeta(meta).session(sessionId).command({
|
|
29
|
+
type: "operator",
|
|
30
|
+
requestId: input.requestId,
|
|
31
|
+
author: input.author,
|
|
32
|
+
text: input.text,
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
errorKind: (error, requestId) => error instanceof ShiitakeError &&
|
|
36
|
+
error.boundary === "contract" &&
|
|
37
|
+
error.cause === "closed" &&
|
|
38
|
+
error.requestId === requestId
|
|
39
|
+
? "closed_request"
|
|
40
|
+
: null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import { installStandaloneAppSet } from "./app-set.js";
|
|
3
|
+
export declare function installApps(args: readonly string[], install?: typeof installStandaloneAppSet): Promise<void>;
|
|
4
|
+
export declare function runInstallAppsCli(args?: readonly string[]): Promise<void>;
|
|
@@ -76,7 +76,7 @@ function timingTable(results) {
|
|
|
76
76
|
];
|
|
77
77
|
return [header, ...rows].map((row) => row.join("\t")).join("\n");
|
|
78
78
|
}
|
|
79
|
-
async function installApps(args) {
|
|
79
|
+
export async function installApps(args, install = installStandaloneAppSet) {
|
|
80
80
|
const { values } = parseArgs({
|
|
81
81
|
args: [...args],
|
|
82
82
|
options: {
|
|
@@ -121,7 +121,7 @@ async function installApps(args) {
|
|
|
121
121
|
source = { kind: "checkout", directory, revision: `git:${revision}` };
|
|
122
122
|
}
|
|
123
123
|
const started = performance.now();
|
|
124
|
-
const
|
|
124
|
+
const { installed, failed } = await install({
|
|
125
125
|
source,
|
|
126
126
|
url,
|
|
127
127
|
key,
|
|
@@ -131,33 +131,43 @@ async function installApps(args) {
|
|
|
131
131
|
},
|
|
132
132
|
});
|
|
133
133
|
const elapsed = performance.now() - started;
|
|
134
|
-
process.stdout.write(`\n${timingTable(
|
|
135
|
-
process.stdout.write(
|
|
134
|
+
process.stdout.write(`\n${timingTable(installed)}\n`);
|
|
135
|
+
process.stdout.write("\nApp\tResult\n");
|
|
136
|
+
for (const app of installed)
|
|
137
|
+
process.stdout.write(`${app.installationId}\tinstalled\n`);
|
|
138
|
+
for (const app of failed)
|
|
139
|
+
process.stdout.write(`${app.installationId}\tfailed: ${app.reason}\n`);
|
|
140
|
+
process.stdout.write(`\nInstalled ${String(installed.length)} Apps; failed ${String(failed.length)} in ${seconds(elapsed)}.${installed.length > 0 ? ` Open ${url}/ to use the installed Apps.` : ""}\n`);
|
|
136
141
|
if (values.timings !== undefined)
|
|
137
142
|
await writeFile(values.timings, JSON.stringify({
|
|
138
143
|
concurrency,
|
|
139
144
|
elapsedMs: elapsed,
|
|
140
|
-
apps:
|
|
145
|
+
apps: installed.map(({ installationId, jobId, timings }) => ({
|
|
141
146
|
installationId,
|
|
142
147
|
jobId,
|
|
143
148
|
timings,
|
|
144
149
|
})),
|
|
150
|
+
failed,
|
|
145
151
|
}, null, 2) + "\n");
|
|
152
|
+
if (failed.length > 0)
|
|
153
|
+
process.exitCode = 1;
|
|
146
154
|
}
|
|
147
155
|
finally {
|
|
148
156
|
await rm(scratch, { recursive: true, force: true });
|
|
149
157
|
}
|
|
150
158
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
159
|
+
export async function runInstallAppsCli(args = process.argv.slice(2)) {
|
|
160
|
+
const [command, ...rest] = args;
|
|
161
|
+
if (command === "--help" || command === "-h") {
|
|
162
|
+
process.stdout.write(usage + "\n" + installReleaseUsage + "\n");
|
|
163
|
+
}
|
|
164
|
+
else if (command !== "install-apps" && command !== "install") {
|
|
165
|
+
process.stderr.write(`${usage}\n`);
|
|
166
|
+
process.exitCode = 2;
|
|
167
|
+
}
|
|
168
|
+
else
|
|
169
|
+
await (command === "install" ? installReleaseCommand(rest) : installApps(rest)).catch((cause) => {
|
|
170
|
+
process.stderr.write(`${cause instanceof Error ? cause.message : String(cause)}\n`);
|
|
171
|
+
process.exitCode = 1;
|
|
172
|
+
});
|
|
158
173
|
}
|
|
159
|
-
else
|
|
160
|
-
await (command === "install" ? installReleaseCommand(rest) : installApps(rest)).catch((cause) => {
|
|
161
|
-
process.stderr.write(`${cause instanceof Error ? cause.message : String(cause)}\n`);
|
|
162
|
-
process.exitCode = 1;
|
|
163
|
-
});
|
|
@@ -268,6 +268,8 @@ export declare class MachineLinkDO extends DurableObject<StandaloneMachineLinkEn
|
|
|
268
268
|
alarm(): Promise<void>;
|
|
269
269
|
fetch(request: Request): Promise<Response>;
|
|
270
270
|
deliver(id: MachineLinkIdentity, caller: MachineLinkCaller, agentId: string, envelope: PostEnvelope): Promise<PostDeliveryResult>;
|
|
271
|
+
deliverRelayDisposition(id: MachineLinkIdentity, relay: MachineLinkIdentity, original: PostEnvelope, reply: PostEnvelope): Promise<PostDeliveryResult>;
|
|
272
|
+
hasStagedDisposition(id: MachineLinkIdentity, original: PostEnvelope, reply: PostEnvelope): Promise<boolean>;
|
|
271
273
|
operation(id: MachineLinkIdentity, caller: MachineLinkCaller, agentId: string, operation: MachineAgentOperation): Promise<import("effect/Schema").Json>;
|
|
272
274
|
applyFence(fact: MachineLinkFenceFact): Promise<MachineLinkFenceResult>;
|
|
273
275
|
preflightErasure(teamId: string, deviceId: string): Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { machineAgentReservations } from "@fungi.computer/shiitake/delegation";
|
|
1
|
+
import { machineAgentCodecs, machineAgentReservations, } from "@fungi.computer/shiitake/delegation";
|
|
2
2
|
import { createMachineEnrollment, MachineEnrollmentRefused, MachineLinkRefused, } from "../machine-link/index.js";
|
|
3
3
|
import { keyAllowsMachineEnroll, } from "../keys.js";
|
|
4
4
|
import { agentIds, createAgentDirectory, } from "@fungi.computer/shiitake/fleet";
|
|
@@ -8,6 +8,7 @@ import { bodyLimit } from "hono/body-limit";
|
|
|
8
8
|
import { validator } from "hono/validator";
|
|
9
9
|
import { machineEnrollmentSchema, machineLinkName, } from "../machine-link/index.js";
|
|
10
10
|
import { createMachineLink } from "../machine-link/workers.js";
|
|
11
|
+
import { deliverUndeliverableMail } from "../machine-link/delivery.js";
|
|
11
12
|
import { defaultTenant, tenantScope } from "../tenant.js";
|
|
12
13
|
import { presentedGitKey } from "../git-auth.js";
|
|
13
14
|
import { standaloneTenancy } from "./tenancy.js";
|
|
@@ -49,6 +50,7 @@ export function standaloneMachineLinkAuthority(env) {
|
|
|
49
50
|
return directory().machineDevice(id);
|
|
50
51
|
},
|
|
51
52
|
currentFence: async (id) => id.teamId !== teamId ? null : directory().machineFence(id),
|
|
53
|
+
agentHome: async (id, agentId) => id.teamId !== teamId ? null : directory().agentHome(agentId),
|
|
52
54
|
directory: async (id) => {
|
|
53
55
|
if (!(await authority.admitDevice(id)).live)
|
|
54
56
|
throw new MachineLinkRefused("actor_not_allowed");
|
|
@@ -69,6 +71,31 @@ export function standaloneMachineLinkAuthority(env) {
|
|
|
69
71
|
throw new MachineLinkRefused("actor_not_allowed");
|
|
70
72
|
return env.MACHINE_LINK.getByName(machineLinkName(next)).deliver(next, caller, facts.targetAgentId, envelope);
|
|
71
73
|
},
|
|
74
|
+
deliverUndeliverable: async (id, original, reply) => {
|
|
75
|
+
if ((await authority.currentFence(id)) === null)
|
|
76
|
+
return "refused";
|
|
77
|
+
const mail = original.event;
|
|
78
|
+
if (mail.from.kind !== "agent")
|
|
79
|
+
return "refused";
|
|
80
|
+
const home = await authority.agentHome(id, mail.from.agentId);
|
|
81
|
+
if (home === null)
|
|
82
|
+
throw new MachineLinkRefused("unavailable");
|
|
83
|
+
return await deliverUndeliverableMail({
|
|
84
|
+
identity: id,
|
|
85
|
+
original,
|
|
86
|
+
reply,
|
|
87
|
+
senderHome: home,
|
|
88
|
+
hosted: env.AGENTS,
|
|
89
|
+
accepted: machineAgentCodecs.accepted,
|
|
90
|
+
machine: async (deviceId, envelope) => {
|
|
91
|
+
const destination = { teamId, deviceId };
|
|
92
|
+
return await env.MACHINE_LINK.getByName(machineLinkName(destination)).deliverRelayDisposition(destination, id, original, envelope);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
verifyRelayDisposition: async (relay, original, reply) => {
|
|
97
|
+
return await env.MACHINE_LINK.getByName(machineLinkName(relay)).hasStagedDisposition(relay, original, reply);
|
|
98
|
+
},
|
|
72
99
|
canErase: async (id) => {
|
|
73
100
|
if (id.teamId !== teamId)
|
|
74
101
|
return false;
|
|
@@ -185,6 +212,12 @@ export class MachineLinkDO extends DurableObject {
|
|
|
185
212
|
deliver(id, caller, agentId, envelope) {
|
|
186
213
|
return this.link.deliver(id, caller, agentId, envelope);
|
|
187
214
|
}
|
|
215
|
+
deliverRelayDisposition(id, relay, original, reply) {
|
|
216
|
+
return this.link.deliverRelayDisposition(id, relay, original, reply);
|
|
217
|
+
}
|
|
218
|
+
hasStagedDisposition(id, original, reply) {
|
|
219
|
+
return this.link.hasStagedDisposition(id, original, reply);
|
|
220
|
+
}
|
|
188
221
|
operation(id, caller, agentId, operation) {
|
|
189
222
|
return this.link.operation(id, caller, agentId, operation);
|
|
190
223
|
}
|
|
@@ -187,7 +187,7 @@ export declare abstract class StandaloneRegistry extends OysterRegistry implemen
|
|
|
187
187
|
appId: string;
|
|
188
188
|
releaseId: string;
|
|
189
189
|
sourceRevision: string;
|
|
190
|
-
status: "running" | "succeeded" | "failed" | "revoked";
|
|
190
|
+
status: "running" | "succeeded" | "failed" | "interrupted" | "revoked";
|
|
191
191
|
detail: string | null;
|
|
192
192
|
release: Release | null;
|
|
193
193
|
installation: import("../app-builds.js").OysterBuildInstallation | null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { type AppCapabilityId } from "@fungi.computer/puffball/app";
|
|
3
3
|
import { type OysterAgents } from "../agents.js";
|
|
4
|
+
import type { LiveOysterKey } from "../keys.js";
|
|
4
5
|
import { type FileSource } from "../files.js";
|
|
5
6
|
import { type OysterAuthResult, type OysterCapability } from "../host.js";
|
|
6
7
|
/** How one capability fixture host admits its single installation. */
|
|
@@ -19,6 +20,7 @@ export type CapabilityFixtureOptions = Readonly<{
|
|
|
19
20
|
visitor?: boolean;
|
|
20
21
|
authenticate?: (request: Request) => Promise<OysterAuthResult>;
|
|
21
22
|
agents?: OysterAgents;
|
|
23
|
+
liveKey?: (keyId: string) => Promise<LiveOysterKey | null>;
|
|
22
24
|
}>;
|
|
23
25
|
/** One Oyster host with a single installation whose capability grants the options shape. */
|
|
24
26
|
export declare function capabilityFixture(options?: CapabilityFixtureOptions): Promise<{
|
|
@@ -84,7 +84,7 @@ export async function capabilityFixture(options = {}) {
|
|
|
84
84
|
if (options.visitor)
|
|
85
85
|
installation = { ...installation, visibility: "public" };
|
|
86
86
|
const readOnly = () => Promise.reject(new Error("Fixture installations are read-only"));
|
|
87
|
-
|
|
87
|
+
let base = {
|
|
88
88
|
bootUi: () => Promise.resolve({ kind: "passed" }),
|
|
89
89
|
auth: {
|
|
90
90
|
resolve: () => Promise.reject(new Error("Capability fixture resolves no actor")),
|
|
@@ -116,6 +116,8 @@ export async function capabilityFixture(options = {}) {
|
|
|
116
116
|
},
|
|
117
117
|
capabilities,
|
|
118
118
|
};
|
|
119
|
+
if (options.liveKey !== undefined)
|
|
120
|
+
base = { ...base, keys: { live: options.liveKey } };
|
|
119
121
|
const host = options.agents === undefined
|
|
120
122
|
? base
|
|
121
123
|
: { ...base, ownerRoutes: agentApprovalRoutes(options.agents) };
|
package/dist/workers-git.js
CHANGED
|
@@ -1117,7 +1117,7 @@ export async function oysterGitWorkerHost(env, options, selected, request) {
|
|
|
1117
1117
|
const base = oysterWorkerHost(env, auth, policyOptions);
|
|
1118
1118
|
const capabilities = [
|
|
1119
1119
|
...(base.capabilities ?? []),
|
|
1120
|
-
...(agents === undefined ? [] : agentCapabilities(agents)),
|
|
1120
|
+
...(agents === undefined ? [] : agentCapabilities(agents, keysLive)),
|
|
1121
1121
|
...(options.capabilities?.(env, workspace, keysLive) ?? []),
|
|
1122
1122
|
];
|
|
1123
1123
|
let gitOptions = { scope };
|
package/dist/workers.d.ts
CHANGED
|
@@ -138,6 +138,7 @@ export declare class OysterRegistry<Env extends OysterWorkerEnv = OysterWorkerEn
|
|
|
138
138
|
};
|
|
139
139
|
readonly reversibility: "reversible" | "irreversible-external";
|
|
140
140
|
readonly requestedAt: number;
|
|
141
|
+
readonly denialNote?: string | undefined;
|
|
141
142
|
readonly evidence?: readonly {
|
|
142
143
|
readonly kind: "artifact";
|
|
143
144
|
readonly ref: string;
|
|
@@ -155,6 +156,7 @@ export declare class OysterRegistry<Env extends OysterWorkerEnv = OysterWorkerEn
|
|
|
155
156
|
}>, actor: HostActor, input: ApprovalAnswerInput): Promise<Readonly<{
|
|
156
157
|
kind: "refused";
|
|
157
158
|
reason: "pending" | "expired" | "not-found" | "not-eligible" | "already-answered" | "stale-revision" | "declined" | "fenced" | "not-granted" | "read-only" | "in-flight" | "outcome-unknown" | "identity-conflict";
|
|
159
|
+
denialNote?: string;
|
|
158
160
|
}> | Readonly<{
|
|
159
161
|
kind: "answered";
|
|
160
162
|
view: import("@fungi.computer/approvals").ApprovalView;
|
|
@@ -166,6 +168,7 @@ export declare class OysterRegistry<Env extends OysterWorkerEnv = OysterWorkerEn
|
|
|
166
168
|
}>, actor: HostActor, input: AutoApproveInput): Promise<Readonly<{
|
|
167
169
|
kind: "refused";
|
|
168
170
|
reason: "pending" | "expired" | "not-found" | "not-eligible" | "already-answered" | "stale-revision" | "declined" | "fenced" | "not-granted" | "read-only" | "in-flight" | "outcome-unknown" | "identity-conflict";
|
|
171
|
+
denialNote?: string;
|
|
169
172
|
}> | Readonly<{
|
|
170
173
|
kind: "updated";
|
|
171
174
|
rule: AutoApproveInput;
|
package/dist/workers.js
CHANGED
|
@@ -48,6 +48,8 @@ import { createOysterRootData } from "./root-data.js";
|
|
|
48
48
|
import { backendCache, resolveBackend } from "./release.js";
|
|
49
49
|
import { ensureAppSourceTables, createOysterAppSourceReceipts, createOysterAppRegistrations, createOysterArtifactsRepositoryMetadataStore, } from "./app-sources.js";
|
|
50
50
|
export { r2ArtifactStore, r2FileSource, ownerKeyAuth };
|
|
51
|
+
/** Issuer-ahead clock allowance at issue only; resolution never adds skew. */
|
|
52
|
+
const BOOT_ISSUER_SKEW_MS = 2_000;
|
|
51
53
|
const recordRow = Schema.Struct({
|
|
52
54
|
installation_id: Schema.String,
|
|
53
55
|
record: Schema.String,
|
|
@@ -433,8 +435,14 @@ export class OysterRegistry extends DurableObject {
|
|
|
433
435
|
const { lease, tokenDigest } = bootCapabilityGrantSchema.parse(input.grant);
|
|
434
436
|
validateCapabilityAssetSet(lease);
|
|
435
437
|
const now = Date.now();
|
|
438
|
+
// The issuer can be at most 2 s ahead of this registry. Apply that
|
|
439
|
+
// allowance only on issue: resolution still uses the stored absolute
|
|
440
|
+
// expiry, and revocation still removes the token after the page check.
|
|
441
|
+
// A lease can therefore live at most 14 s on this clock, not be renewed
|
|
442
|
+
// or replayed once expired.
|
|
436
443
|
if (lease.expiresAt <= now ||
|
|
437
|
-
lease.expiresAt >
|
|
444
|
+
lease.expiresAt >
|
|
445
|
+
now + BOOT_DEADLINE_MS + BOOT_TRANSPORT_GRACE_MS + BOOT_ISSUER_SKEW_MS)
|
|
438
446
|
throw new Error("Boot capability lease is outside its deadline");
|
|
439
447
|
this.ctx.storage.sql.exec("DELETE FROM boot_capabilities WHERE expires_at <= ?", now);
|
|
440
448
|
const count = this.ctx.storage.sql
|
package/docs/api/agents.md
CHANGED
|
@@ -358,16 +358,20 @@ optional bearer.
|
|
|
358
358
|
### agentCapabilities()
|
|
359
359
|
|
|
360
360
|
```ts
|
|
361
|
-
function agentCapabilities(
|
|
361
|
+
function agentCapabilities(
|
|
362
|
+
agents,
|
|
363
|
+
liveKey,
|
|
364
|
+
): readonly Readonly<object & object>[];
|
|
362
365
|
```
|
|
363
366
|
|
|
364
367
|
Agent adapters admitted against each App's declared capabilities.
|
|
365
368
|
|
|
366
369
|
#### Parameters
|
|
367
370
|
|
|
368
|
-
| Parameter | Type
|
|
369
|
-
| --------- |
|
|
370
|
-
| `agents` | [`OysterAgents`](#oysteragents)
|
|
371
|
+
| Parameter | Type |
|
|
372
|
+
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
373
|
+
| `agents` | [`OysterAgents`](#oysteragents) |
|
|
374
|
+
| `liveKey` | (`keyId`) => `Promise`\< \| `Readonly`\<\{ `keyId`: `string`; `owner`: `string`; `grantVersion`: `number`; `grants`: readonly `Readonly`\<\{ `capability`: [`OysterKeyCapability`](keys.md#oysterkeycapability); `targets`: `GrantTargets`; \}\>[]; \}\> \| `null`\> |
|
|
371
375
|
|
|
372
376
|
#### Returns
|
|
373
377
|
|