@fungi.computer/oyster 0.3.2 → 0.4.1
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/agent-computer-source.d.ts +42 -0
- package/dist/agent-computer-source.js +88 -0
- package/dist/agent-computers.d.ts +104 -0
- package/dist/agent-computers.js +227 -0
- package/dist/agents.d.ts +10 -0
- package/dist/agents.js +33 -0
- package/dist/app-builds.d.ts +1 -1
- package/dist/app-builds.js +16 -6
- package/dist/boot-capability.d.ts +2 -1
- package/dist/boot-capability.js +7 -5
- package/dist/browser/boot.d.ts +2 -2
- package/dist/browser/boot.js +10 -3
- package/dist/browser/browser.d.ts +1 -1
- package/dist/browser/browser.js +2 -1
- package/dist/browser/cdp-client.d.ts +2 -0
- package/dist/browser/errors.d.ts +2 -2
- package/dist/browser/errors.js +2 -2
- package/dist/browser/index.d.ts +3 -2
- package/dist/browser/index.js +3 -2
- package/dist/browser/page.js +11 -4
- package/dist/browser/sessions.d.ts +7 -4
- package/dist/browser/sessions.js +159 -0
- package/dist/browser/url-policy.d.ts +4 -2
- package/dist/browser/url-policy.js +4 -2
- 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 +223 -5
- package/dist/build/candidate-diagnostic.d.ts +2 -0
- package/dist/build/candidate-diagnostic.js +16 -0
- package/dist/computer-control-port.d.ts +26 -0
- package/dist/computer-control-port.js +1 -0
- package/dist/host.d.ts +3 -0
- package/dist/machine-link/delivery.d.ts +1 -1
- package/dist/machine-link/delivery.js +2 -3
- package/dist/machine-link/host.d.ts +37 -6
- package/dist/machine-link/host.js +209 -14
- package/dist/machine-link/link.d.ts +13 -1
- package/dist/machine-link/link.js +20 -7
- package/dist/mcp.d.ts +144 -10
- package/dist/mcp.js +130 -13
- package/dist/page/computer-handles.d.ts +3 -3
- package/dist/page-script.js +1 -1
- package/dist/standalone/agent-computers.d.ts +29 -0
- package/dist/standalone/agent-computers.js +101 -0
- package/dist/standalone/agent-mail.d.ts +41 -18
- package/dist/standalone/agent-model-choice.d.ts +29 -0
- package/dist/standalone/agent-model-choice.js +72 -0
- package/dist/standalone/agents.d.ts +44 -4
- package/dist/standalone/agents.js +502 -46
- package/dist/standalone/app-set.d.ts +8 -1
- package/dist/standalone/app-set.js +46 -31
- package/dist/standalone/computer-control.d.ts +4 -0
- package/dist/standalone/computer-control.js +62 -0
- package/dist/standalone/computer-fleet.d.ts +1 -0
- package/dist/standalone/computer-fleet.js +1 -1
- package/dist/standalone/computer.d.ts +21 -2
- package/dist/standalone/computer.js +12 -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 +10 -0
- package/dist/standalone/machine-link.js +6 -0
- package/dist/standalone/registry.d.ts +10 -1
- package/dist/standalone/registry.js +9 -0
- package/dist/standalone/worker.js +12 -0
- package/dist/standalone/workflow-binding.d.ts +2 -2
- package/dist/standalone/workflow-host.d.ts +11 -11
- package/dist/standalone/workflows.d.ts +22 -22
- package/dist/storage-frame.d.ts +3 -3
- package/dist/workers-git.d.ts +2 -0
- package/dist/workers-git.js +2 -0
- package/dist/workers.d.ts +8 -8
- package/dist/workers.js +9 -1
- package/docs/api/agents.md +66 -0
- package/docs/api/apps.md +104 -104
- package/docs/api/build-contract.md +35 -35
- package/docs/api/build.md +12 -12
- package/docs/api/host.md +61 -61
- package/docs/api/mcp.md +127 -13
- package/docs/api/root-data.md +1 -1
- package/docs/api/tenancy.md +1 -1
- package/docs/api/workers-git.md +3 -2
- package/docs/api/workers.md +81 -83
- package/package.json +26 -18
|
@@ -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");
|
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
|
};
|
|
@@ -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 {
|