@fungi.computer/oyster 0.3.2 → 0.4.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 (69) hide show
  1. package/dist/agent-computer-source.d.ts +42 -0
  2. package/dist/agent-computer-source.js +88 -0
  3. package/dist/agent-computers.d.ts +104 -0
  4. package/dist/agent-computers.js +227 -0
  5. package/dist/agents.d.ts +10 -0
  6. package/dist/agents.js +33 -0
  7. package/dist/browser/boot.d.ts +2 -2
  8. package/dist/browser/boot.js +10 -3
  9. package/dist/browser/browser.d.ts +1 -1
  10. package/dist/browser/browser.js +2 -1
  11. package/dist/browser/cdp-client.d.ts +2 -0
  12. package/dist/browser/errors.d.ts +2 -2
  13. package/dist/browser/errors.js +2 -2
  14. package/dist/browser/index.d.ts +3 -2
  15. package/dist/browser/index.js +3 -2
  16. package/dist/browser/page.js +11 -4
  17. package/dist/browser/sessions.d.ts +7 -4
  18. package/dist/browser/sessions.js +159 -0
  19. package/dist/browser/url-policy.d.ts +4 -2
  20. package/dist/browser/url-policy.js +4 -2
  21. package/dist/computer-control-port.d.ts +26 -0
  22. package/dist/computer-control-port.js +1 -0
  23. package/dist/host.d.ts +3 -0
  24. package/dist/machine-link/delivery.d.ts +1 -1
  25. package/dist/machine-link/delivery.js +2 -3
  26. package/dist/machine-link/host.d.ts +37 -6
  27. package/dist/machine-link/host.js +209 -14
  28. package/dist/machine-link/link.d.ts +13 -1
  29. package/dist/machine-link/link.js +20 -7
  30. package/dist/mcp.d.ts +144 -10
  31. package/dist/mcp.js +130 -13
  32. package/dist/page/computer-handles.d.ts +3 -3
  33. package/dist/page-script.js +1 -1
  34. package/dist/standalone/agent-computers.d.ts +29 -0
  35. package/dist/standalone/agent-computers.js +101 -0
  36. package/dist/standalone/agent-mail.d.ts +41 -18
  37. package/dist/standalone/agent-model-choice.d.ts +26 -0
  38. package/dist/standalone/agent-model-choice.js +72 -0
  39. package/dist/standalone/agents.d.ts +44 -4
  40. package/dist/standalone/agents.js +502 -46
  41. package/dist/standalone/computer-control.d.ts +4 -0
  42. package/dist/standalone/computer-control.js +62 -0
  43. package/dist/standalone/computer-fleet.d.ts +1 -0
  44. package/dist/standalone/computer-fleet.js +1 -1
  45. package/dist/standalone/computer.d.ts +21 -2
  46. package/dist/standalone/computer.js +12 -0
  47. package/dist/standalone/machine-link.d.ts +10 -0
  48. package/dist/standalone/machine-link.js +6 -0
  49. package/dist/standalone/registry.d.ts +9 -0
  50. package/dist/standalone/registry.js +9 -0
  51. package/dist/standalone/worker.js +12 -0
  52. package/dist/standalone/workflow-binding.d.ts +2 -2
  53. package/dist/standalone/workflow-host.d.ts +11 -11
  54. package/dist/standalone/workflows.d.ts +22 -22
  55. package/dist/storage-frame.d.ts +3 -3
  56. package/dist/workers-git.d.ts +2 -0
  57. package/dist/workers-git.js +2 -0
  58. package/dist/workers.d.ts +8 -8
  59. package/docs/api/agents.md +66 -0
  60. package/docs/api/apps.md +104 -104
  61. package/docs/api/build-contract.md +35 -35
  62. package/docs/api/build.md +12 -12
  63. package/docs/api/host.md +61 -61
  64. package/docs/api/mcp.md +127 -13
  65. package/docs/api/root-data.md +1 -1
  66. package/docs/api/tenancy.md +1 -1
  67. package/docs/api/workers-git.md +3 -2
  68. package/docs/api/workers.md +81 -83
  69. package/package.json +25 -17
@@ -0,0 +1,42 @@
1
+ import { type ComputerConnectionOptions, type FilesPort, type KnapsackComputer } from "@fungi.computer/knapsack";
2
+ import type { CapabilitySource } from "@fungi.computer/mycelium";
3
+ import type { AgentComputerAdmission, AgentComputerProjection } from "./agent-computers.js";
4
+ /** The host's captured authority namespace, Agent and attachment projection. */
5
+ export type AgentComputerSnapshot = Readonly<{
6
+ scopeId: string;
7
+ agentId: string;
8
+ projection: AgentComputerProjection;
9
+ }>;
10
+ /** Host adapters supply transport, not a second attachment owner. */
11
+ export type AgentComputerPorts = Readonly<{
12
+ files: FilesPort;
13
+ computer: KnapsackComputer;
14
+ }>;
15
+ export type AgentComputerConnection = Readonly<{
16
+ scopeId: string;
17
+ agentId: string;
18
+ attachment: AgentComputerProjection["attachments"][number];
19
+ /** Re-read on every operation; captured handles cannot follow a reattachment. */
20
+ admission: () => AgentComputerAdmission | null;
21
+ }>;
22
+ /** A host-owned projection reader and the transport for its captured attachments. */
23
+ export type AgentComputerSourceOptions = Readonly<{
24
+ read: () => AgentComputerSnapshot | null;
25
+ subscribe: (invalidate: () => void) => () => void;
26
+ connect: (connection: AgentComputerConnection) => AgentComputerPorts;
27
+ onUnavailable?: (detail: {
28
+ computerId: string;
29
+ reason: unknown;
30
+ }) => void;
31
+ }>;
32
+ /** Capture each named Computer and its Files port under the same authority. */
33
+ export declare function readAgentComputerConnections(options: Pick<AgentComputerSourceOptions, "read" | "connect">): readonly (ComputerConnectionOptions & {
34
+ attachmentId: string;
35
+ files: {
36
+ port: FilesPort;
37
+ };
38
+ })[];
39
+ /** Discover the captured connections; current admission remains a per-call host check. */
40
+ export declare function createAgentComputerSource(options: AgentComputerSourceOptions): CapabilitySource;
41
+ /** Advertise only the denied shape; no attachment lookup or Computer wake. */
42
+ export declare function createAgentComputerSurface(): CapabilitySource;
@@ -0,0 +1,88 @@
1
+ import { Knapsack, } from "@fungi.computer/knapsack";
2
+ import { computerCapabilities, computerValueSchema, } from "@fungi.computer/computer-contract";
3
+ /** Capture each named Computer and its Files port under the same authority. */
4
+ export function readAgentComputerConnections(options) {
5
+ return capturedConnections(options, options.read());
6
+ }
7
+ function capturedConnections(options, captured) {
8
+ if (captured === null)
9
+ return [];
10
+ return captured.projection.attachments.map((attachment) => {
11
+ const ports = options.connect({
12
+ scopeId: captured.scopeId,
13
+ agentId: captured.agentId,
14
+ attachment,
15
+ admission: () => {
16
+ const current = options.read();
17
+ if (current?.scopeId !== captured.scopeId ||
18
+ current.agentId !== captured.agentId)
19
+ return null;
20
+ return (current.projection.attachments.find((candidate) => candidate.attachmentId === attachment.attachmentId &&
21
+ candidate.computerId === attachment.computerId &&
22
+ candidate.version === attachment.version)?.admission ?? null);
23
+ },
24
+ });
25
+ return {
26
+ id: `computer:${attachment.attachmentId}`,
27
+ attachmentId: attachment.attachmentId,
28
+ name: attachment.name,
29
+ namespace: ["computers"],
30
+ files: { port: ports.files },
31
+ computer: ports.computer,
32
+ };
33
+ });
34
+ }
35
+ /** Discover the captured connections; current admission remains a per-call host check. */
36
+ export function createAgentComputerSource(options) {
37
+ return Knapsack.computers({
38
+ id: "agent-computers",
39
+ subscribe: options.subscribe,
40
+ onUnavailable: (detail) => {
41
+ console.warn("Agent Computer unavailable", detail);
42
+ options.onUnavailable?.(detail);
43
+ },
44
+ read: () => {
45
+ const captured = options.read();
46
+ return {
47
+ version: JSON.stringify([
48
+ captured?.scopeId ?? null,
49
+ captured?.agentId ?? null,
50
+ captured?.projection.revision ?? 0,
51
+ ]),
52
+ connections: capturedConnections(options, captured),
53
+ };
54
+ },
55
+ });
56
+ }
57
+ const refusedBeforeRun = () => Promise.reject(new Error("The tool gate refuses this call before it runs"));
58
+ /** Advertise only the denied shape; no attachment lookup or Computer wake. */
59
+ export function createAgentComputerSurface() {
60
+ return Knapsack.computers({
61
+ id: "agent-computers-surface",
62
+ subscribe: () => () => undefined,
63
+ onUnavailable: () => undefined,
64
+ read: () => ({
65
+ version: "surface",
66
+ connections: [
67
+ {
68
+ id: "computer:surface",
69
+ name: "computer",
70
+ namespace: ["computers"],
71
+ files: {
72
+ port: {
73
+ read: refusedBeforeRun,
74
+ list: refusedBeforeRun,
75
+ glob: refusedBeforeRun,
76
+ grep: refusedBeforeRun,
77
+ replace: refusedBeforeRun,
78
+ },
79
+ },
80
+ computer: {
81
+ getCapabilities: () => Promise.resolve(computerValueSchema.parse(computerCapabilities())),
82
+ call: refusedBeforeRun,
83
+ },
84
+ },
85
+ ],
86
+ }),
87
+ });
88
+ }
@@ -0,0 +1,104 @@
1
+ import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
2
+ import { z } from "zod";
3
+ declare const attachmentIdentitySchema: z.ZodObject<{
4
+ attachmentId: z.ZodString;
5
+ agentId: z.ZodString;
6
+ computerId: z.ZodString;
7
+ version: z.ZodNumber;
8
+ }, z.core.$strict>;
9
+ export declare const projectionSchema: z.ZodObject<{
10
+ revision: z.ZodNumber;
11
+ attachments: z.ZodArray<z.ZodObject<{
12
+ attachmentId: z.ZodString;
13
+ agentId: z.ZodString;
14
+ computerId: z.ZodString;
15
+ version: z.ZodNumber;
16
+ name: z.ZodString;
17
+ admission: z.ZodNullable<z.ZodObject<{
18
+ grantId: z.ZodString;
19
+ agentId: z.ZodString;
20
+ attachmentId: z.ZodString;
21
+ operations: z.ZodArray<z.ZodEnum<{
22
+ "computer.list": "computer.list";
23
+ "computer.readFile": "computer.readFile";
24
+ "computer.writeFile": "computer.writeFile";
25
+ "computer.execute": "computer.execute";
26
+ }>>;
27
+ attachmentVersion: z.ZodNumber;
28
+ revision: z.ZodNumber;
29
+ expiresAt: z.ZodNumber;
30
+ }, z.core.$strict>>;
31
+ }, z.core.$strict>>;
32
+ }, z.core.$strict>;
33
+ export type AgentComputerProjection = z.output<typeof projectionSchema>;
34
+ /** The parsed admission fields shared by the projection and the host's read port. */
35
+ export type AgentComputerAdmissionFields = NonNullable<AgentComputerProjection["attachments"][number]["admission"]>;
36
+ /** Host-issued attachment admission; the host owns its policy and delivery. */
37
+ export type AgentComputerAdmission = Readonly<Omit<AgentComputerAdmissionFields, "operations"> & {
38
+ operations: readonly AgentComputerAdmissionFields["operations"][number][];
39
+ }>;
40
+ export type ComputerAttachmentInput = Readonly<{
41
+ agentId: string;
42
+ attachmentId: string;
43
+ computerId: string;
44
+ requestId: string;
45
+ }>;
46
+ export type AttachComputer = ComputerAttachmentInput;
47
+ export type DetachComputer = ComputerAttachmentInput;
48
+ export type AttachmentVersion = Readonly<{
49
+ computerId: string;
50
+ agentId: string;
51
+ attachmentId: string;
52
+ version: number;
53
+ }>;
54
+ /** Renew a held Computer admission before its delivered grant expires. */
55
+ export declare const AGENT_ADMISSION_RENEW_BEFORE_MS = 30000;
56
+ export type AgentComputers = Readonly<{
57
+ attachComputer(c: AttachComputer): AgentComputerProjection;
58
+ detachComputer(c: DetachComputer): AgentComputerProjection;
59
+ /** The attachment owner's transactionally advanced projection generation. */
60
+ revision(agentId: string): number;
61
+ read(agentId: string): AgentComputerProjection;
62
+ refresh(agentId: string, force?: boolean): AgentComputerProjection;
63
+ /** Every live attachment, for host-wide admission renewal or suspension. */
64
+ attachments(): readonly AttachmentVersion[];
65
+ /** Git admission reads live links through their owner, without foreign SQL. */
66
+ attachedComputers(agentId: string): readonly string[];
67
+ attachedAgents(computerId: string): readonly string[];
68
+ nextRenewal(): number | null;
69
+ /** Void every live attachment's decision by moving its version past it. */
70
+ onLifecycle(status: "active" | "suspended", revision: number): void;
71
+ }>;
72
+ export declare const attachComputerInputSchema: z.ZodObject<{
73
+ agentId: z.ZodString;
74
+ attachmentId: z.ZodString;
75
+ computerId: z.ZodString;
76
+ requestId: z.ZodString;
77
+ }, z.core.$strict>;
78
+ export declare const detachComputerInputSchema: z.ZodObject<{
79
+ agentId: z.ZodString;
80
+ attachmentId: z.ZodString;
81
+ computerId: z.ZodString;
82
+ requestId: z.ZodString;
83
+ }, z.core.$strict>;
84
+ export declare const readAgentComputersInputSchema: z.ZodObject<{
85
+ agentId: z.ZodString;
86
+ }, z.core.$strict>;
87
+ export declare function createAgentComputers(options: Readonly<{
88
+ sql: WatchdogSqliteOwner["sql"];
89
+ transactionSync: <T>(body: () => T) => T;
90
+ /** Record Computer denial in the same transaction that detaches this relationship. */
91
+ revoke: (requestId: string, attachment: z.output<typeof attachmentIdentitySchema>) => void;
92
+ /** Read the host-owned friendly name for one attached Computer. */
93
+ readComputerName: (computerId: string) => string;
94
+ /** Stage a host-owned projection in the mutation transaction. */
95
+ enqueueProjection?: (agentId: string, projection: AgentComputerProjection) => void;
96
+ ensureAdmissions?: (attachments: AgentComputerProjection["attachments"]) => boolean;
97
+ readAdmission?: (attachment: {
98
+ agentId: string;
99
+ attachmentId: string;
100
+ computerId: string;
101
+ version: number;
102
+ }) => AgentComputerAdmission | null;
103
+ }>): AgentComputers;
104
+ export {};
@@ -0,0 +1,227 @@
1
+ import { z } from "zod";
2
+ const text = z.string().min(1).max(256);
3
+ const noChange = Symbol("no-change");
4
+ const attachmentRowSchema = z.strictObject({
5
+ attachment_id: z.string(),
6
+ agent_id: z.string(),
7
+ computer_id: z.string(),
8
+ version: z.number().int().positive(),
9
+ });
10
+ const attachmentIdentitySchema = z.strictObject({
11
+ attachmentId: text,
12
+ agentId: text,
13
+ computerId: text,
14
+ version: z.number().int().nonnegative(),
15
+ });
16
+ const attachmentProjectionSchema = attachmentIdentitySchema.extend({
17
+ name: text,
18
+ admission: z
19
+ .strictObject({
20
+ grantId: text,
21
+ agentId: text,
22
+ attachmentId: text,
23
+ operations: z.array(z.enum([
24
+ "computer.list",
25
+ "computer.readFile",
26
+ "computer.writeFile",
27
+ "computer.execute",
28
+ ])),
29
+ attachmentVersion: z.number().int().positive(),
30
+ revision: z.number().int().positive(),
31
+ expiresAt: z.number().int().positive(),
32
+ })
33
+ .nullable(),
34
+ });
35
+ export const projectionSchema = z.strictObject({
36
+ revision: z.number().int().nonnegative(),
37
+ attachments: z.array(attachmentProjectionSchema),
38
+ });
39
+ class AgentComputersError extends Error {
40
+ code;
41
+ constructor(code) {
42
+ super(code);
43
+ this.code = code;
44
+ }
45
+ }
46
+ /** Renew a held Computer admission before its delivered grant expires. */
47
+ export const AGENT_ADMISSION_RENEW_BEFORE_MS = 30_000;
48
+ const computerAttachmentInputSchema = z.strictObject({
49
+ agentId: text,
50
+ attachmentId: text,
51
+ computerId: text,
52
+ requestId: text,
53
+ });
54
+ export const attachComputerInputSchema = computerAttachmentInputSchema;
55
+ export const detachComputerInputSchema = computerAttachmentInputSchema;
56
+ export const readAgentComputersInputSchema = z.strictObject({ agentId: text });
57
+ export function createAgentComputers(options) {
58
+ const { sql, transactionSync } = options;
59
+ const ensure = () => {
60
+ sql.exec("CREATE TABLE IF NOT EXISTS hub_agent_computers (attachment_id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, computer_id TEXT NOT NULL, version INTEGER NOT NULL, attached INTEGER NOT NULL CHECK (attached IN (0, 1)), UNIQUE (agent_id, computer_id)) STRICT");
61
+ sql.exec("CREATE TABLE IF NOT EXISTS hub_agent_computer_revisions (agent_id TEXT PRIMARY KEY, revision INTEGER NOT NULL) STRICT");
62
+ sql.exec("CREATE TABLE IF NOT EXISTS hub_agent_computer_receipts (request_id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, operation TEXT NOT NULL, input TEXT NOT NULL, attachment_id TEXT, revision INTEGER NOT NULL, result TEXT NOT NULL) STRICT");
63
+ };
64
+ transactionSync(ensure);
65
+ const revision = (agentId) => z
66
+ .number()
67
+ .int()
68
+ .nonnegative()
69
+ .parse(sql
70
+ .exec("SELECT revision FROM hub_agent_computer_revisions WHERE agent_id = ?", agentId)
71
+ .toArray()[0]?.revision ?? 0);
72
+ const read = (agentId) => {
73
+ const attachments = sql
74
+ .exec("SELECT attachment_id, agent_id, computer_id, version FROM hub_agent_computers WHERE agent_id = ? AND attached = 1 ORDER BY attachment_id", agentId)
75
+ .toArray()
76
+ .map((row) => attachmentRowSchema.parse(row))
77
+ .map((r) => ({
78
+ attachmentId: r.attachment_id,
79
+ agentId: r.agent_id,
80
+ computerId: r.computer_id,
81
+ version: r.version,
82
+ name: options.readComputerName(r.computer_id),
83
+ admission: options.readAdmission?.({
84
+ attachmentId: r.attachment_id,
85
+ agentId: r.agent_id,
86
+ computerId: r.computer_id,
87
+ version: r.version,
88
+ }) ?? null,
89
+ }));
90
+ return projectionSchema.parse({ revision: revision(agentId), attachments });
91
+ };
92
+ const commit = (agentId, operation, requestId, payload, mutate) => transactionSync(() => {
93
+ const input = JSON.stringify({ agentId, operation, payload });
94
+ const old = sql
95
+ .exec("SELECT agent_id, operation, input, result FROM hub_agent_computer_receipts WHERE request_id = ?", requestId)
96
+ .toArray()[0];
97
+ if (old) {
98
+ if (old.agent_id !== agentId ||
99
+ old.operation !== operation ||
100
+ old.input !== input)
101
+ throw new AgentComputersError("request_conflict");
102
+ return projectionSchema.parse(JSON.parse(z.string().parse(old.result)));
103
+ }
104
+ const changed = mutate();
105
+ const before = read(agentId);
106
+ if (changed === noChange) {
107
+ sql.exec("INSERT INTO hub_agent_computer_receipts (request_id,agent_id,operation,input,attachment_id,revision,result) VALUES (?,?,?,?,NULL,?,?)", requestId, agentId, operation, input, before.revision, JSON.stringify(before));
108
+ return before;
109
+ }
110
+ const revision = before.revision + 1;
111
+ sql.exec("INSERT INTO hub_agent_computer_revisions (agent_id,revision) VALUES (?,?) ON CONFLICT (agent_id) DO UPDATE SET revision=excluded.revision", agentId, revision);
112
+ const beforeAdmission = read(agentId);
113
+ options.ensureAdmissions?.(beforeAdmission.attachments);
114
+ const current = read(agentId);
115
+ sql.exec("INSERT INTO hub_agent_computer_receipts (request_id,agent_id,operation,input,attachment_id,revision,result) VALUES (?,?,?,?,?,?,?)", requestId, agentId, operation, input, changed, revision, JSON.stringify(current));
116
+ options.enqueueProjection?.(agentId, current);
117
+ return current;
118
+ });
119
+ const attachComputer = (raw) => {
120
+ const c = attachComputerInputSchema.parse(raw);
121
+ const payload = { attachmentId: c.attachmentId, computerId: c.computerId };
122
+ return commit(c.agentId, "attachComputer", c.requestId, payload, () => {
123
+ const row = sql
124
+ .exec("SELECT agent_id, computer_id, attached FROM hub_agent_computers WHERE attachment_id=?", c.attachmentId)
125
+ .toArray()[0];
126
+ if (row !== undefined &&
127
+ (row.agent_id !== c.agentId || row.computer_id !== c.computerId))
128
+ throw new AgentComputersError("attachment_identity_conflict");
129
+ let changed = false;
130
+ if (row?.attached === 0) {
131
+ sql.exec("UPDATE hub_agent_computers SET attached=1,version=version+1 WHERE attachment_id=?", c.attachmentId);
132
+ changed = true;
133
+ }
134
+ else if (row === undefined) {
135
+ sql.exec("INSERT INTO hub_agent_computers (attachment_id,agent_id,computer_id,version,attached) VALUES (?,?,?,1,1)", c.attachmentId, c.agentId, c.computerId);
136
+ changed = true;
137
+ }
138
+ return changed ? c.attachmentId : noChange;
139
+ });
140
+ };
141
+ const detachComputer = (raw) => {
142
+ const c = detachComputerInputSchema.parse(raw);
143
+ return commit(c.agentId, "detachComputer", c.requestId, {
144
+ attachmentId: c.attachmentId,
145
+ computerId: c.computerId,
146
+ }, () => {
147
+ const stored = sql
148
+ .exec("SELECT agent_id, computer_id, attached, version FROM hub_agent_computers WHERE attachment_id=?", c.attachmentId)
149
+ .toArray()[0];
150
+ if (stored !== undefined &&
151
+ (stored.agent_id !== c.agentId || stored.computer_id !== c.computerId))
152
+ throw new AgentComputersError("attachment_identity_conflict");
153
+ if (stored === undefined || stored.attached === 0)
154
+ return noChange;
155
+ sql.exec("UPDATE hub_agent_computers SET attached=0,version=version+1 WHERE attachment_id=?", c.attachmentId);
156
+ options.revoke(c.requestId, attachmentIdentitySchema.parse({
157
+ attachmentId: c.attachmentId,
158
+ agentId: stored.agent_id,
159
+ computerId: stored.computer_id,
160
+ version: stored.version,
161
+ }));
162
+ return c.attachmentId;
163
+ });
164
+ };
165
+ const refresh = (agentId, force = false) => transactionSync(() => {
166
+ const before = read(agentId);
167
+ const renewed = options.ensureAdmissions?.(before.attachments) ?? false;
168
+ if (!force && !renewed)
169
+ return before;
170
+ const revision = before.revision + 1;
171
+ sql.exec("INSERT INTO hub_agent_computer_revisions (agent_id,revision) VALUES (?,?) ON CONFLICT (agent_id) DO UPDATE SET revision=excluded.revision", agentId, revision);
172
+ const current = read(agentId);
173
+ options.enqueueProjection?.(agentId, current);
174
+ return current;
175
+ });
176
+ const attachments = () => sql
177
+ .exec("SELECT agent_id,attachment_id,computer_id,version FROM hub_agent_computers WHERE attached=1 ORDER BY agent_id,attachment_id")
178
+ .toArray()
179
+ .map((row) => attachmentRowSchema.parse(row))
180
+ .map((row) => ({
181
+ agentId: row.agent_id,
182
+ attachmentId: row.attachment_id,
183
+ computerId: row.computer_id,
184
+ version: row.version,
185
+ }));
186
+ const advanceAttachedVersions = () => {
187
+ sql.exec("UPDATE hub_agent_computers SET version=version+1 WHERE attached=1");
188
+ };
189
+ return {
190
+ attachComputer,
191
+ detachComputer,
192
+ revision,
193
+ read,
194
+ refresh,
195
+ attachments,
196
+ attachedComputers: (agentId) => sql
197
+ .exec("SELECT computer_id FROM hub_agent_computers WHERE agent_id=? AND attached=1", agentId)
198
+ .toArray()
199
+ .map(({ computer_id }) => z.string().parse(computer_id)),
200
+ attachedAgents: (computerId) => sql
201
+ .exec("SELECT agent_id FROM hub_agent_computers WHERE computer_id=? AND attached=1", computerId)
202
+ .toArray()
203
+ .map(({ agent_id }) => z.string().parse(agent_id)),
204
+ nextRenewal: () => {
205
+ const agentIds = new Set(attachments().map(({ agentId }) => agentId));
206
+ const deadlines = [...agentIds].flatMap((agentId) => read(agentId).attachments.map(({ admission }) => admission === null
207
+ ? Date.now()
208
+ : admission.expiresAt - AGENT_ADMISSION_RENEW_BEFORE_MS));
209
+ return deadlines.length === 0 ? null : Math.min(...deadlines);
210
+ },
211
+ onLifecycle: (status, revision) => {
212
+ const current = attachments();
213
+ if (status === "suspended") {
214
+ for (const attachment of current)
215
+ options.revoke(JSON.stringify([
216
+ "team-suspension",
217
+ revision,
218
+ attachment.agentId,
219
+ attachment.attachmentId,
220
+ ]), attachment);
221
+ advanceAttachedVersions();
222
+ }
223
+ for (const agentId of new Set(current.map(({ agentId }) => agentId)))
224
+ refresh(agentId, true);
225
+ },
226
+ };
227
+ }
package/dist/agents.d.ts CHANGED
@@ -12,6 +12,8 @@ import * as Schema from "effect/Schema";
12
12
  import { type OysterCapability, type OysterEnv, type OysterPrincipal } from "./host.js";
13
13
  import type { HostActor } from "@fungi.computer/puffball/app";
14
14
  import type { Avatar } from "@fungi.computer/sprite/avatar";
15
+ import type { AgentModelChoice, AgentModelRefusal } from "./standalone/agent-model-choice.js";
16
+ export type { AgentModelChoice, AgentModelRefusal, } from "./standalone/agent-model-choice.js";
15
17
  import type { MemberInboxItem, MemberInboxSession } from "@fungi.computer/puffball/inbox";
16
18
  /** One agent as Apps see it, in Agar's `appAgentSchema` shape. */
17
19
  export type OysterAgent = Readonly<{
@@ -87,6 +89,14 @@ export type OysterAgents = Readonly<{
87
89
  setAvatar(agentId: string, avatar: Avatar, principal: OysterPrincipal): Promise<OysterAgent | Readonly<{
88
90
  error: "not_found" | "not_granted";
89
91
  }>>;
92
+ /** Owner-only, secret-free Agent default; an unknown Agent cannot be configured. */
93
+ readModelChoice(agentId: string): Promise<AgentModelChoice | null>;
94
+ /** Parse the owner's JSON through the deployable Agent's model preference schema. */
95
+ decodeModelChoice(json: string): AgentModelChoice | null;
96
+ /** Persist a supported model and source reference for this Agent, or return an explicit refusal. */
97
+ setModelChoice(agentId: string, choice: AgentModelChoice, principal: OysterPrincipal): Promise<AgentModelChoice | Readonly<{
98
+ error: "not_found" | "not_granted" | AgentModelRefusal;
99
+ }>>;
90
100
  /** This Agent's App-tool policy status, or `null` for no such Agent. */
91
101
  policyStatus(agentId: string): Promise<AgentToolPolicyStatus | null>;
92
102
  /** Read this Agent's stored App-tool approval; absence requires re-approval. A key's App access is its own grants (Ear Tag), not this policy. */
package/dist/agents.js CHANGED
@@ -290,6 +290,39 @@ async function approvePolicy(agents, principal, actor, agentId, body) {
290
290
  /** Host-owned direct approval under the admitted owner; never an App capability. */
291
291
  export function agentApprovalRoutes(agents) {
292
292
  return new Hono()
293
+ .get("/api/agent-models/:agentId", async (context) => {
294
+ if (context.req.raw.headers.has("X-Oyster-Installation") ||
295
+ !oysterPrincipalIsOwner(context.get("principal")))
296
+ return refusal(403, "Owner only");
297
+ const agentId = context.req.param("agentId");
298
+ if (!agentIdPattern.test(agentId))
299
+ return refusal(404, "No such agent");
300
+ const choice = await agents.readModelChoice(agentId);
301
+ return choice === null
302
+ ? refusal(404, "No such agent")
303
+ : Response.json(choice);
304
+ })
305
+ .put("/api/agent-models/:agentId", async (context) => {
306
+ if (context.req.raw.headers.has("X-Oyster-Installation") ||
307
+ !oysterPrincipalIsOwner(context.get("principal")))
308
+ return refusal(403, "Owner only");
309
+ const agentId = context.req.param("agentId");
310
+ if (!agentIdPattern.test(agentId))
311
+ return refusal(404, "No such agent");
312
+ const input = agents.decodeModelChoice(await context.req.text());
313
+ if (input === null)
314
+ return refusal(400, "Invalid model choice");
315
+ const choice = await agents.setModelChoice(agentId, input, context.get("principal"));
316
+ if ("error" in choice && choice.error === "invalid_model_choice")
317
+ return refusal(400, "Invalid model choice");
318
+ return "error" in choice
319
+ ? refusal(choice.error === "not_found"
320
+ ? 404
321
+ : choice.error === "not_granted"
322
+ ? 403
323
+ : 409, choice.error)
324
+ : Response.json(choice);
325
+ })
293
326
  .get("/api/agent-policies/:agentId", async (context) => {
294
327
  if (context.req.raw.headers.has("X-Oyster-Installation"))
295
328
  return refusal(403, "Owner direct only");
@@ -8,8 +8,8 @@ import type { BrowserSessions } from "./sessions.js";
8
8
  * (`BrowserSessions`), never a meter, so a boot check is never billed to a
9
9
  * Team, and the agent tool never receives it. It accepts no URL: it builds
10
10
  * `<origin>/_boot/<releaseId>/index.html` itself from the host's configured
11
- * origin. Its dedicated provider session is pinned to that host by the
12
- * provider guardrail (a host-level backstop). The browser's request gate
11
+ * origin. Browser Run also pins that host through a provider guardrail;
12
+ * Chromium contexts have no provider host allowlist. The browser's request gate
13
13
  * fails every request whose exact origin (scheme, host, port) differs, on
14
14
  * the page and on every frame it admits, before that frame runs. The
15
15
  * capability token is added by that gate, and only to exact-origin
@@ -12,8 +12,8 @@ import { attachPage, navigatePage, pageLocation } from "./page.js";
12
12
  * (`BrowserSessions`), never a meter, so a boot check is never billed to a
13
13
  * Team, and the agent tool never receives it. It accepts no URL: it builds
14
14
  * `<origin>/_boot/<releaseId>/index.html` itself from the host's configured
15
- * origin. Its dedicated provider session is pinned to that host by the
16
- * provider guardrail (a host-level backstop). The browser's request gate
15
+ * origin. Browser Run also pins that host through a provider guardrail;
16
+ * Chromium contexts have no provider host allowlist. The browser's request gate
17
17
  * fails every request whose exact origin (scheme, host, port) differs, on
18
18
  * the page and on every frame it admits, before that frame runs. The
19
19
  * capability token is added by that gate, and only to exact-origin
@@ -102,7 +102,11 @@ const responseSchema = z.object({
102
102
  });
103
103
  const loadingFailedSchema = z.object({ requestId: z.string() });
104
104
  const createdSchema = z.object({
105
- targetInfo: z.object({ targetId: z.string(), type: z.string() }),
105
+ targetInfo: z.object({
106
+ targetId: z.string(),
107
+ type: z.string(),
108
+ browserContextId: z.string().optional(),
109
+ }),
106
110
  });
107
111
  const attachedSchema = z.object({
108
112
  sessionId: z.string(),
@@ -355,6 +359,9 @@ function observe(client, recorder, pinned, target, children, pages) {
355
359
  const created = createdSchema.safeParse(params);
356
360
  if (!created.success || created.data.targetInfo.type !== "page")
357
361
  return;
362
+ if (client.browserContextId !== undefined &&
363
+ created.data.targetInfo.browserContextId !== client.browserContextId)
364
+ return;
358
365
  const { targetId } = created.data.targetInfo;
359
366
  if (!pages.sealed)
360
367
  pages.known.add(targetId);
@@ -63,7 +63,7 @@ export type BrowserLimits = Readonly<{
63
63
  maxConcurrentPerTeam: number;
64
64
  /** A lease older than this is reaped even if active. */
65
65
  maxLifetimeMs: number;
66
- /** A lease idle this long is reaped; also the provider keep-alive. */
66
+ /** Reaped on the next operation; Browser Run also uses it as keep-alive. */
67
67
  idleTimeoutMs: number;
68
68
  /** Per-navigation load deadline. */
69
69
  navigationTimeoutMs?: number;
@@ -56,7 +56,8 @@ export function createBrowser(host, limits) {
56
56
  const attempts = (pending?.attempts ?? 0) + 1;
57
57
  if (attempts > maxTeardownAttempts) {
58
58
  teardowns.delete(sessionId);
59
- // The provider keep-alive (the idle timeout) is the last backstop.
59
+ // Browser Run has an idle backstop. Chromium does not: a context
60
+ // abandoned here persists until Chromium restarts.
60
61
  host.report({
61
62
  kind: "teardown_abandoned",
62
63
  teamId,
@@ -34,6 +34,8 @@ export type HttpFetch = (url: string, init?: RequestInit) => Promise<Response>;
34
34
  */
35
35
  export declare function openCdpSocket(fetcher: HttpFetch, url: string, headers: Readonly<Record<string, string>>, signal: AbortSignal): Promise<CdpSocket>;
36
36
  export type CdpClient = Readonly<{
37
+ /** A Chromium context-scoped host supplies this; Browser Run leaves it absent. */
38
+ browserContextId?: string;
37
39
  /** One command/response round trip, optionally flattened into `sessionId`. */
38
40
  call: (method: string, params: Readonly<Record<string, Json>>, options?: Readonly<{
39
41
  sessionId?: string;
@@ -3,7 +3,7 @@ declare const CloudBrowserUrlDenied_base: new <A extends Record<string, any> = {
3
3
  } & Readonly<A>;
4
4
  /**
5
5
  * The cloud-browser backend's closed, typed failure channel. Every backend
6
- * (today only Cloudflare Browser Run) throws exclusively from this union;
6
+ * (Cloudflare Browser Run or on-box Chromium) throws from this union;
7
7
  * `agent-cloud-browser-source.ts` is the one place that maps it onto
8
8
  * Mycelium's `CapabilityFailed`, so the model only ever sees a bounded,
9
9
  * owner-admitted message (never a raw provider error body or stack).
@@ -17,7 +17,7 @@ export declare class CloudBrowserUrlDenied extends CloudBrowserUrlDenied_base<{
17
17
  declare const CloudBrowserUpstreamError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
18
18
  readonly _tag: "CloudBrowserUpstreamError";
19
19
  } & Readonly<A>;
20
- /** The provider (Cloudflare Browser Run) answered with a failure. */
20
+ /** The configured provider answered with a failure or is unreachable. */
21
21
  export declare class CloudBrowserUpstreamError extends CloudBrowserUpstreamError_base<{
22
22
  readonly op: string;
23
23
  readonly status: number | null;
@@ -1,7 +1,7 @@
1
1
  import * as Data from "effect/Data";
2
2
  /**
3
3
  * The cloud-browser backend's closed, typed failure channel. Every backend
4
- * (today only Cloudflare Browser Run) throws exclusively from this union;
4
+ * (Cloudflare Browser Run or on-box Chromium) throws from this union;
5
5
  * `agent-cloud-browser-source.ts` is the one place that maps it onto
6
6
  * Mycelium's `CapabilityFailed`, so the model only ever sees a bounded,
7
7
  * owner-admitted message (never a raw provider error body or stack).
@@ -9,7 +9,7 @@ import * as Data from "effect/Data";
9
9
  /** The target URL failed policy before any provider call was made. */
10
10
  export class CloudBrowserUrlDenied extends Data.TaggedError("CloudBrowserUrlDenied") {
11
11
  }
12
- /** The provider (Cloudflare Browser Run) answered with a failure. */
12
+ /** The configured provider answered with a failure or is unreachable. */
13
13
  export class CloudBrowserUpstreamError extends Data.TaggedError("CloudBrowserUpstreamError") {
14
14
  }
15
15
  /** A call did not complete within its deadline. */
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * `@fungi.computer/oyster/browser`: the one agent browser for the Hub and a
3
3
  * standalone Oyster. A host builds the port from a provider (`browserRunSessions`
4
- * or `bindingSessions`, plus Browser Run Quick Reads when it has them), its
4
+ * or `bindingSessions` or `onboxBrowserSessions` on staging, plus Browser Run
5
+ * Quick Reads when it has them), its
5
6
  * required metering owner and an event sink; `createBrowser` owns lease
6
7
  * ownership, egress policy and billing order; `browserCapabilitySource` is
7
8
  * the agent's `cloudBrowser` tool. Raw CDP stays internal to this directory.
@@ -13,4 +14,4 @@ export type { LeaseOwner } from "./lease-registry.js";
13
14
  export { cloudBrowserBounds, extractInputSchema, extractResultSchema, linksResultSchema, markdownInputSchema, markdownResultSchema, navigateResultSchema, openResultSchema, scrapeInputSchema, scrapeResultSchema, snapshotResultSchema, type BinaryResult, type CloudBrowserAck, type CloudBrowserClosed, type CloudBrowserOperation, type CloudBrowserOperationResult, type ExtractInput, type ExtractResult, type LinksResult, type MarkdownInput, type MarkdownResult, type NavigateResult, type OpenResult, type ScrapeInput, type ScrapeResult, type SnapshotResult, } from "./operations.js";
14
15
  export type { CdpSocket, HttpFetch } from "./cdp-client.js";
15
16
  export { browserRunQuickReads, type BrowserRunQuickReadsConfig, type QuickRead, type QuickReads, } from "./quick-reads.js";
16
- export { bindingSessions, browserRunSessions, type BrowserBinding, type BrowserSessions, type CdpConnect, } from "./sessions.js";
17
+ export { bindingSessions, browserRunSessions, onboxBrowserSessions, type BrowserBinding, type BrowserSessions, type CdpConnect, } from "./sessions.js";