@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
package/dist/agents.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* transport (`/browser/…`), served by the adapter behind the same App grants.
|
|
8
8
|
*/
|
|
9
9
|
import { Hono } from "hono";
|
|
10
|
+
import { type LiveOysterKey } from "./keys.js";
|
|
10
11
|
import type { AppToolPolicy } from "./apps/tools.js";
|
|
11
12
|
import * as Schema from "effect/Schema";
|
|
12
13
|
import { type OysterCapability, type OysterEnv, type OysterPrincipal } from "./host.js";
|
|
@@ -125,7 +126,7 @@ export type OysterAgents = Readonly<{
|
|
|
125
126
|
visit(agentId: string, sessionId: string): Promise<boolean>;
|
|
126
127
|
}>;
|
|
127
128
|
/** Agent adapters admitted against each App's declared capabilities. */
|
|
128
|
-
export declare function agentCapabilities(agents: OysterAgents): readonly OysterCapability[];
|
|
129
|
+
export declare function agentCapabilities(agents: OysterAgents, liveKey: (keyId: string) => Promise<LiveOysterKey | null>): readonly OysterCapability[];
|
|
129
130
|
/** Host-owned direct approval under the admitted owner; never an App capability. */
|
|
130
131
|
export declare function agentApprovalRoutes(agents: OysterAgents): Hono<OysterEnv>;
|
|
131
132
|
/**
|
package/dist/agents.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* transport (`/browser/…`), served by the adapter behind the same App grants.
|
|
8
8
|
*/
|
|
9
9
|
import { Hono } from "hono";
|
|
10
|
+
import { keyAgentAccess } from "./keys.js";
|
|
10
11
|
import * as Schema from "effect/Schema";
|
|
11
12
|
import { z } from "zod";
|
|
12
13
|
import { oysterPrincipalIsOwner, } from "./host.js";
|
|
@@ -51,8 +52,8 @@ function refusal(status, error) {
|
|
|
51
52
|
return Response.json({ error }, { status });
|
|
52
53
|
}
|
|
53
54
|
/** Agent adapters admitted against each App's declared capabilities. */
|
|
54
|
-
export function agentCapabilities(agents) {
|
|
55
|
-
const routes = createAgentRoutes(agents);
|
|
55
|
+
export function agentCapabilities(agents, liveKey) {
|
|
56
|
+
const routes = createAgentRoutes(agents, liveKey);
|
|
56
57
|
const shared = { base: "/", routes };
|
|
57
58
|
return [
|
|
58
59
|
{
|
|
@@ -134,9 +135,41 @@ export function agentCapabilities(agents) {
|
|
|
134
135
|
},
|
|
135
136
|
];
|
|
136
137
|
}
|
|
137
|
-
function createAgentRoutes(agents) {
|
|
138
|
+
function createAgentRoutes(agents, liveKey) {
|
|
138
139
|
const routes = new Hono();
|
|
139
|
-
|
|
140
|
+
// Every Agent and Session lookup shares this live principal-scoped door.
|
|
141
|
+
const access = async (principal, agentId) => {
|
|
142
|
+
if (oysterPrincipalIsOwner(principal))
|
|
143
|
+
return true;
|
|
144
|
+
if (principal.actor.kind !== "team-key")
|
|
145
|
+
return false;
|
|
146
|
+
const key = await liveKey(principal.actor.keyId);
|
|
147
|
+
return key !== null && keyAgentAccess(key, agentId);
|
|
148
|
+
};
|
|
149
|
+
routes.use("/agents/:agentId/*", async (context, next) => {
|
|
150
|
+
if (!(await access(context.env.principal, context.req.param("agentId"))))
|
|
151
|
+
return refusal(403, "Agent access denied");
|
|
152
|
+
await next();
|
|
153
|
+
return undefined;
|
|
154
|
+
});
|
|
155
|
+
routes.use("/agent-policies/:agentId", async (context, next) => {
|
|
156
|
+
if (!(await access(context.env.principal, context.req.param("agentId"))))
|
|
157
|
+
return refusal(403, "Agent access denied");
|
|
158
|
+
await next();
|
|
159
|
+
return undefined;
|
|
160
|
+
});
|
|
161
|
+
routes.get("/agents", async (context) => {
|
|
162
|
+
const listed = await agents.list();
|
|
163
|
+
const actor = context.env.principal.actor;
|
|
164
|
+
if (oysterPrincipalIsOwner(context.env.principal))
|
|
165
|
+
return Response.json(listed);
|
|
166
|
+
if (actor.kind !== "team-key")
|
|
167
|
+
return Response.json([]);
|
|
168
|
+
const key = await liveKey(actor.keyId);
|
|
169
|
+
return Response.json(key === null
|
|
170
|
+
? []
|
|
171
|
+
: listed.filter((agent) => keyAgentAccess(key, agent.agentId)));
|
|
172
|
+
});
|
|
140
173
|
routes.post("/agents", async (context) => {
|
|
141
174
|
const { principal } = context.env;
|
|
142
175
|
if (!oysterPrincipalIsOwner(principal))
|
|
@@ -216,19 +249,37 @@ function createAgentRoutes(agents) {
|
|
|
216
249
|
const response = await agents.fetch(agentId, new Request(target, incoming));
|
|
217
250
|
return response ?? refusal(404, "No such agent");
|
|
218
251
|
});
|
|
219
|
-
routes.get("/member/inbox", async () => {
|
|
252
|
+
routes.get("/member/inbox", async (context) => {
|
|
220
253
|
const inbox = await agents.inbox();
|
|
254
|
+
const principal = context.env.principal;
|
|
255
|
+
if (oysterPrincipalIsOwner(principal))
|
|
256
|
+
return Response.json({
|
|
257
|
+
items: inbox.items,
|
|
258
|
+
next: null,
|
|
259
|
+
cursor: inbox.revision,
|
|
260
|
+
});
|
|
261
|
+
if (principal.actor.kind !== "team-key")
|
|
262
|
+
return refusal(403, "Agent access denied");
|
|
263
|
+
const key = await liveKey(principal.actor.keyId);
|
|
221
264
|
return Response.json({
|
|
222
|
-
items:
|
|
265
|
+
items: key === null
|
|
266
|
+
? []
|
|
267
|
+
: inbox.items.filter((item) => "agentId" in item && keyAgentAccess(key, item.agentId)),
|
|
223
268
|
next: null,
|
|
224
|
-
|
|
269
|
+
// The global revision changes for ungranted Agents too. Key feeds have
|
|
270
|
+
// no scoped event stream, so expose neither that revision nor its events.
|
|
271
|
+
cursor: 0,
|
|
225
272
|
});
|
|
226
273
|
});
|
|
227
|
-
routes.get("/member/inbox/notices", () =>
|
|
274
|
+
routes.get("/member/inbox/notices", (context) => oysterPrincipalIsOwner(context.env.principal)
|
|
275
|
+
? Response.json({ items: [], next: null })
|
|
276
|
+
: refusal(403, "Agent access denied"));
|
|
228
277
|
routes.post("/member/inbox/visits", async (context) => {
|
|
229
278
|
const body = Schema.decodeUnknownOption(visitSchema)(await context.req.json());
|
|
230
279
|
if (body._tag === "None")
|
|
231
280
|
return refusal(400, "Invalid visit");
|
|
281
|
+
if (!(await access(context.env.principal, body.value.agentId)))
|
|
282
|
+
return refusal(403, "Agent access denied");
|
|
232
283
|
return (await agents.visit(body.value.agentId, body.value.sessionId))
|
|
233
284
|
? new Response(null, { status: 204 })
|
|
234
285
|
: refusal(404, "No such Session");
|
|
@@ -236,6 +287,10 @@ function createAgentRoutes(agents) {
|
|
|
236
287
|
// The feed is small (one person's agents), so a change is announced as a
|
|
237
288
|
// `reset` and the client re-reads the whole inbox.
|
|
238
289
|
routes.get("/member/inbox/events", (context) => {
|
|
290
|
+
// The unscoped revision also changes for other Agents. Do not expose it
|
|
291
|
+
// until a principal-scoped revision exists.
|
|
292
|
+
if (!oysterPrincipalIsOwner(context.env.principal))
|
|
293
|
+
return refusal(403, "Agent access denied");
|
|
239
294
|
const after = Number(context.req.query("after") ?? "0");
|
|
240
295
|
const signal = context.req.raw.signal;
|
|
241
296
|
const encoder = new TextEncoder();
|
package/dist/app-builds.d.ts
CHANGED
|
@@ -97,7 +97,7 @@ export type OysterBuildView = Readonly<{
|
|
|
97
97
|
appId: string;
|
|
98
98
|
releaseId: string;
|
|
99
99
|
sourceRevision: string;
|
|
100
|
-
status: "running" | "succeeded" | "failed" | "revoked";
|
|
100
|
+
status: "running" | "succeeded" | "failed" | "interrupted" | "revoked";
|
|
101
101
|
detail: string | null;
|
|
102
102
|
release: Release | null;
|
|
103
103
|
installation: OysterBuildInstallation | null;
|
package/dist/app-builds.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import * as Schema from "effect/Schema";
|
|
20
20
|
import * as Data from "effect/Data";
|
|
21
21
|
import * as Option from "effect/Option";
|
|
22
|
+
import { candidateFailureDiagnostic } from "./build/candidate-diagnostic.js";
|
|
22
23
|
import { parseArtifactRef, parseReleaseId, } from "@fungi.computer/puffball";
|
|
23
24
|
import { appIdSchema, releaseSchema, sha256DigestSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
|
|
24
25
|
import { capturedPinnedRecipeSchema, normalizeAppPublisherRecipe, digestPinnedRecipe, fitsPinnedRecipe, pinnedRecipeObjectBound, } from "./build/effect.js";
|
|
@@ -116,7 +117,13 @@ const jobRow = Schema.Struct({
|
|
|
116
117
|
credential_id: Schema.String,
|
|
117
118
|
upload_grant_digest: Schema.String,
|
|
118
119
|
request_id: Schema.NullOr(Schema.String),
|
|
119
|
-
status: Schema.Literals([
|
|
120
|
+
status: Schema.Literals([
|
|
121
|
+
"running",
|
|
122
|
+
"succeeded",
|
|
123
|
+
"failed",
|
|
124
|
+
"interrupted",
|
|
125
|
+
"revoked",
|
|
126
|
+
]),
|
|
120
127
|
artifact_digest: Schema.NullOr(Schema.String),
|
|
121
128
|
artifact_bytes: Schema.NullOr(Schema.Number),
|
|
122
129
|
detail: Schema.NullOr(Schema.String),
|
|
@@ -186,12 +193,12 @@ export function createOysterAppBuilds(options) {
|
|
|
186
193
|
// A job outlives the owner instance that started it: the Computer keeps
|
|
187
194
|
// building and its own reports (objects, candidate, status) settle the
|
|
188
195
|
// job, so an owner restart must not fail it. A job that reaches its expiry
|
|
189
|
-
// without a result can never settle, so it
|
|
196
|
+
// without a result can never settle, so it is interrupted instead of reading
|
|
190
197
|
// as running for as long as anyone asks.
|
|
191
198
|
const failedInstallation = `CASE WHEN installation IS NULL THEN NULL ELSE
|
|
192
199
|
json_object('kind', 'failed', 'installationId', json_extract(installation, '$.installationId'),
|
|
193
200
|
'detail', COALESCE(?, 'Build did not succeed')) END`;
|
|
194
|
-
const expire = () => owner.sql.exec(`UPDATE oyster_build_jobs SET installation=${failedInstallation}, status='
|
|
201
|
+
const expire = () => owner.sql.exec(`UPDATE oyster_build_jobs SET installation=${failedInstallation}, status='interrupted', detail=? WHERE status='running' AND expires_at<=?`, "Build expired without a result", "Build expired without a result", now());
|
|
195
202
|
expire();
|
|
196
203
|
const decodeRecipe = (recipe) => Schema.decodeUnknownSync(recipeCodec)(recipe);
|
|
197
204
|
const grantAdmits = async (job, grant) => sameDigest(job.upload_grant_digest, await sha256Hex(new TextEncoder().encode(grant)));
|
|
@@ -269,6 +276,9 @@ export function createOysterAppBuilds(options) {
|
|
|
269
276
|
return Promise.resolve();
|
|
270
277
|
},
|
|
271
278
|
execute: async (input) => {
|
|
279
|
+
// The Computer's connection may close without a terminal execution
|
|
280
|
+
// reply. A missing output is not an exit; a rejected RPC carrying an
|
|
281
|
+
// Error (including an admission refusal) remains a definite failure.
|
|
272
282
|
const result = await options.computer.port.runBuild({
|
|
273
283
|
jobId: input.jobId,
|
|
274
284
|
command: input.command,
|
|
@@ -298,10 +308,10 @@ export function createOysterAppBuilds(options) {
|
|
|
298
308
|
});
|
|
299
309
|
}
|
|
300
310
|
catch (cause) {
|
|
301
|
-
//
|
|
302
|
-
//
|
|
311
|
+
// An unknown output can belong to a live builder. Its reports can
|
|
312
|
+
// still succeed; the existing build expiry interrupts it if abandoned.
|
|
303
313
|
if (!(cause instanceof BuildOutcomeUnknown))
|
|
304
|
-
settle(jobId, "failed", cause
|
|
314
|
+
settle(jobId, "failed", candidateFailureDiagnostic(cause));
|
|
305
315
|
}
|
|
306
316
|
return view(required(jobId));
|
|
307
317
|
};
|
package/dist/apps/gate.d.ts
CHANGED
|
@@ -36,6 +36,10 @@ export declare const gateRequestSchema: z.ZodLazy<z.ZodDiscriminatedUnion<[z.Zod
|
|
|
36
36
|
kind: z.ZodLiteral<"capability-route">;
|
|
37
37
|
capability: z.ZodPipe<z.ZodUnknown, z.ZodTransform<"approvals" | "agent.list" | "agent.manage" | "agent.rename" | "agent.avatar" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "llm.generation" | "media.generation" | "games:servers" | "git.repository" | "apps.open" | "apps.call" | "app.storage" | "app.events.emit" | "team.workbenchHost" | "marketplace.browse" | "marketplace.purchase" | "workflow.run" | "workflow.publish" | "workflow.arm" | "world" | "channels.read" | "channels.personal.manage", unknown>>;
|
|
38
38
|
operation: z.ZodString;
|
|
39
|
+
effect: z.ZodEnum<{
|
|
40
|
+
read: "read";
|
|
41
|
+
write: "write";
|
|
42
|
+
}>;
|
|
39
43
|
}, z.core.$strict>, z.ZodObject<{
|
|
40
44
|
kind: z.ZodLiteral<"binding">;
|
|
41
45
|
binding: z.ZodPipe<z.ZodUnknown, z.ZodTransform<"BACKGROUND" | "GIT" | "EVENTS" | "APPS" | "WORKFLOWS" | "APP_STORAGE" | "MEDIA" | "COMPUTER" | "INBOX" | "AGENTS", unknown>>;
|
package/dist/apps/gate.js
CHANGED
|
@@ -26,6 +26,7 @@ export const gateRequestSchema = z.lazy(() => z.discriminatedUnion("kind", [
|
|
|
26
26
|
kind: z.literal("capability-route"),
|
|
27
27
|
capability: ownerCodec(appCapabilityIdSchema),
|
|
28
28
|
operation: z.string().min(1),
|
|
29
|
+
effect: z.enum(["read", "write"]),
|
|
29
30
|
}),
|
|
30
31
|
z.strictObject({
|
|
31
32
|
kind: z.literal("binding"),
|
|
@@ -259,6 +260,8 @@ export function admits(facts, rawRequest) {
|
|
|
259
260
|
case "capability-route": {
|
|
260
261
|
if (visitor)
|
|
261
262
|
return refusal("visitor_read_only");
|
|
263
|
+
if (request.effect === "write" && facts.access?.level === "read")
|
|
264
|
+
return refusal("level_too_low");
|
|
262
265
|
if (!request.operation.startsWith("route:") ||
|
|
263
266
|
!facts.declaration.capabilities.includes(request.capability) ||
|
|
264
267
|
facts.declaredOperations.capabilities
|
|
@@ -75,10 +75,11 @@ export declare function issueBootCapability(input: Readonly<{
|
|
|
75
75
|
ui: BootUi;
|
|
76
76
|
bucket: BootCapabilityBucket;
|
|
77
77
|
capabilities: BootCapabilityStore;
|
|
78
|
-
|
|
78
|
+
now: () => number;
|
|
79
79
|
}>): Promise<Readonly<{
|
|
80
80
|
token: string;
|
|
81
81
|
leaseId: string;
|
|
82
|
+
deadlineAt: number;
|
|
82
83
|
}>>;
|
|
83
84
|
/** What one boot page report needs from its host. */
|
|
84
85
|
export type BootPageReportInput = Readonly<{
|
package/dist/boot-capability.js
CHANGED
|
@@ -73,11 +73,14 @@ export async function issueBootCapability(input) {
|
|
|
73
73
|
throw new BootGateError("ui_mount_failed");
|
|
74
74
|
byteLengths.push(head.size);
|
|
75
75
|
}
|
|
76
|
+
// Metadata may take longer than the page budget; start the lease only once
|
|
77
|
+
// every asset has been verified, immediately before issuing it.
|
|
78
|
+
const deadlineAt = input.now() + BOOT_DEADLINE_MS;
|
|
76
79
|
const lease = {
|
|
77
80
|
id: bootCapabilityIds.lease.generate(),
|
|
78
81
|
identity: input.ui.identity,
|
|
79
82
|
base: input.ui.base,
|
|
80
|
-
expiresAt:
|
|
83
|
+
expiresAt: deadlineAt + BOOT_TRANSPORT_GRACE_MS,
|
|
81
84
|
assets: capabilityAssets(input.ui.assets, byteLengths),
|
|
82
85
|
};
|
|
83
86
|
validateCapabilityAssetSet(lease);
|
|
@@ -87,7 +90,7 @@ export async function issueBootCapability(input) {
|
|
|
87
90
|
.replace(/=+$/u, "");
|
|
88
91
|
const tokenDigest = createHash("sha256").update(token, "utf8").digest("hex");
|
|
89
92
|
await input.capabilities.issue({ lease, tokenDigest });
|
|
90
|
-
return { token, leaseId: lease.id };
|
|
93
|
+
return { token, leaseId: lease.id, deadlineAt };
|
|
91
94
|
}
|
|
92
95
|
/**
|
|
93
96
|
* One UI page check for a Release, for every host, reading no UI bytes:
|
|
@@ -102,14 +105,13 @@ export async function reportBootPage(input) {
|
|
|
102
105
|
return { kind: "passed" };
|
|
103
106
|
if (input.check === null || input.origin === null)
|
|
104
107
|
return { kind: "not_checked", code: "boot_browser_binding_missing" };
|
|
105
|
-
const deadlineAt = input.now() + BOOT_DEADLINE_MS;
|
|
106
108
|
let issued;
|
|
107
109
|
try {
|
|
108
110
|
issued = await issueBootCapability({
|
|
109
111
|
ui,
|
|
110
112
|
bucket: input.bucket,
|
|
111
113
|
capabilities: input.capabilities,
|
|
112
|
-
|
|
114
|
+
now: input.now,
|
|
113
115
|
});
|
|
114
116
|
}
|
|
115
117
|
catch (cause) {
|
|
@@ -123,7 +125,7 @@ export async function reportBootPage(input) {
|
|
|
123
125
|
origin: input.origin,
|
|
124
126
|
releaseId: ui.identity.releaseId,
|
|
125
127
|
capability: { header: BOOT_CAPABILITY_HEADER, token: issued.token },
|
|
126
|
-
deadlineAt,
|
|
128
|
+
deadlineAt: issued.deadlineAt,
|
|
127
129
|
});
|
|
128
130
|
}
|
|
129
131
|
finally {
|