@fungi.computer/oyster 0.6.0 → 0.8.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/README.md +133 -78
- package/dist/agent-archive-projection.d.ts +8 -0
- package/dist/agent-archive-projection.js +49 -0
- package/dist/agent-schedules.d.ts +61 -0
- package/dist/agent-schedules.js +19 -0
- package/dist/agents.d.ts +35 -14
- package/dist/agents.js +93 -144
- package/dist/app-builds.d.ts +7 -2
- package/dist/app-builds.js +19 -2
- package/dist/approval-capability.d.ts +1 -1
- package/dist/approval-capability.js +11 -16
- package/dist/approval-home.d.ts +2 -1
- package/dist/approval-home.js +5 -3
- package/dist/apps/catalog.js +1 -1
- package/dist/apps/gate.d.ts +4 -7
- package/dist/apps/gate.js +14 -29
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/installation.d.ts +4 -42
- package/dist/apps/installation.js +7 -75
- package/dist/apps/operations.js +0 -10
- package/dist/apps/permissions.d.ts +18 -8
- package/dist/apps/permissions.js +7 -1
- package/dist/auth.d.ts +1 -24
- package/dist/auth.js +9 -39
- package/dist/authorization.d.ts +307 -0
- package/dist/authorization.js +542 -0
- package/dist/boot-host-script.js +1 -1
- package/dist/build/build-jobs.d.ts +2 -2
- package/dist/build/candidate-builder.d.ts +2 -0
- package/dist/build/candidate-builder.js +5 -1
- package/dist/build/candidate-cli.js +3 -0
- package/dist/build/candidate-cli.mjs +70 -22
- package/dist/build/job-log.d.ts +23 -0
- package/dist/build/job-log.js +88 -0
- package/dist/build/local-build.d.ts +9 -1
- package/dist/build/local-build.js +11 -9
- package/dist/cloudflare.d.ts +4 -1
- package/dist/cloudflare.js +1 -1
- package/dist/files.js +9 -8
- package/dist/git-admission.d.ts +3 -3
- package/dist/git-admission.js +4 -6
- package/dist/git-door.d.ts +8 -7
- package/dist/git-door.js +18 -17
- package/dist/git.d.ts +1 -1
- package/dist/host.d.ts +33 -40
- package/dist/host.js +156 -275
- package/dist/installation-execution.js +14 -5
- package/dist/installation-operations.d.ts +4 -180
- package/dist/installation-operations.js +1 -5
- package/dist/installation-store-generation.d.ts +4 -1
- package/dist/installation-store-generation.js +59 -6
- package/dist/internal/delivery.js +0 -1
- package/dist/internal/host-gate.d.ts +3 -2
- package/dist/internal/host-gate.js +5 -18
- package/dist/internal/invocation-timing.d.ts +16 -0
- package/dist/internal/invocation-timing.js +23 -0
- package/dist/internal/registry-installations.js +1 -0
- package/dist/invocation.d.ts +15 -14
- package/dist/invocation.js +58 -26
- package/dist/keys.d.ts +2 -19
- package/dist/keys.js +1 -37
- package/dist/mcp.d.ts +15 -27
- package/dist/mcp.js +105 -155
- package/dist/page/agent-browser.d.ts +15 -0
- package/dist/page/agent-browser.js +162 -0
- package/dist/page/app-permissions-port.d.ts +3 -0
- package/dist/page/app-permissions-port.js +43 -0
- package/dist/page/app-permissions.d.ts +11 -0
- package/dist/page/app-permissions.js +296 -0
- package/dist/page/isolated-page-theme.d.ts +2 -0
- package/dist/page/isolated-page-theme.js +11 -0
- package/dist/page/mount.d.ts +2 -13
- package/dist/page/mount.js +7 -93
- package/dist/page-script.d.ts +0 -3
- package/dist/page-script.js +3 -4
- package/dist/standalone/agent-archive-owner.d.ts +42 -0
- package/dist/standalone/agent-archive-owner.js +116 -0
- package/dist/standalone/agent-archive-schedules.d.ts +6 -0
- package/dist/standalone/agent-archive-schedules.js +76 -0
- package/dist/standalone/agent-archive.d.ts +31 -0
- package/dist/standalone/agent-archive.js +67 -0
- package/dist/standalone/agent-computers.d.ts +7 -6
- package/dist/standalone/agent-computers.js +4 -7
- package/dist/standalone/agent-mail.d.ts +2 -2
- package/dist/standalone/agents.d.ts +186 -20
- package/dist/standalone/agents.js +251 -30
- package/dist/standalone/app-builds-binding.js +1 -1
- package/dist/standalone/app-set.js +2 -54
- package/dist/standalone/app-source.d.ts +3 -4
- package/dist/standalone/app-source.js +12 -9
- package/dist/standalone/builds.d.ts +2 -14
- package/dist/standalone/builds.js +15 -13
- package/dist/standalone/computer-fleet.d.ts +4 -4
- package/dist/standalone/computer-fleet.js +13 -42
- package/dist/standalone/computer.d.ts +4 -3
- package/dist/standalone/computer.js +52 -53
- package/dist/standalone/dashboard-approval.d.ts +10 -0
- package/dist/standalone/dashboard-approval.js +66 -0
- package/dist/standalone/machine-link.d.ts +4 -221
- package/dist/standalone/machine-link.js +21 -15
- package/dist/standalone/media.d.ts +1 -0
- package/dist/standalone/media.js +20 -14
- package/dist/standalone/owner-routes.d.ts +16 -0
- package/dist/standalone/owner-routes.js +21 -0
- package/dist/standalone/permission-resources.d.ts +5 -0
- package/dist/standalone/permission-resources.js +31 -0
- package/dist/standalone/registry.d.ts +7 -0
- package/dist/standalone/registry.js +3 -0
- package/dist/standalone/tenancy.d.ts +0 -3
- package/dist/standalone/tenancy.js +2 -5
- package/dist/standalone/worker-routes.d.ts +19 -0
- package/dist/standalone/worker-routes.js +9 -0
- package/dist/standalone/worker.d.ts +1 -1
- package/dist/standalone/worker.js +27 -35
- package/dist/standalone/workflow-binding.d.ts +10 -3
- package/dist/standalone/workflow-binding.js +22 -5
- package/dist/standalone/workflow-git-capture.js +2 -1
- package/dist/standalone/workflow-host.d.ts +8 -103
- package/dist/standalone/workflow-host.js +1 -1
- package/dist/standalone/workflow-node-capture.js +2 -2
- package/dist/standalone/workflow-root-authority.d.ts +1 -1
- package/dist/standalone/workflow-root-authority.js +1 -1
- package/dist/standalone/workflow-schedules.d.ts +31 -4
- package/dist/standalone/workflow-schedules.js +18 -5
- package/dist/standalone/workflow-webhooks.d.ts +4 -18
- package/dist/standalone/workflow-webhooks.js +5 -5
- package/dist/standalone/workflows.d.ts +53 -27
- package/dist/standalone/workflows.js +9 -3
- package/dist/storage-frame.d.ts +6 -6
- package/dist/testing/capabilities.d.ts +7 -6
- package/dist/testing/capabilities.js +3 -6
- package/dist/testing/installation.d.ts +0 -1
- package/dist/testing/installation.js +2 -5
- package/dist/workers-git.d.ts +12 -4
- package/dist/workers-git.js +38 -35
- package/dist/workers.d.ts +9 -1
- package/dist/workers.js +44 -8
- package/dist/workflows.d.ts +4 -0
- package/dist/workflows.js +2 -0
- package/docs/api/README.md +7 -0
- package/docs/api/agent-schedules.md +176 -0
- package/docs/api/agents.md +34 -13
- package/docs/api/app-storage.md +1 -1
- package/docs/api/apps.md +323 -225
- package/docs/api/auth.md +0 -55
- package/docs/api/authorization.md +1300 -0
- package/docs/api/build.md +42 -38
- package/docs/api/cloudflare.md +3 -3
- package/docs/api/git.md +20 -11
- package/docs/api/host.md +64 -192
- package/docs/api/index.md +1 -1
- package/docs/api/invocation.md +14 -14
- package/docs/api/keys.md +1 -98
- package/docs/api/mcp.md +43 -40
- package/docs/api/modules.md +2 -0
- package/docs/api/page/mount.md +16 -19
- package/docs/api/workers-git.md +46 -12
- package/docs/api/workers.md +111 -222
- package/package.json +41 -22
- package/dist/page/computer-target.d.ts +0 -3
- package/dist/page/computer-target.js +0 -9
- package/dist/standalone/channel-session-door.d.ts +0 -4
- package/dist/standalone/channel-session-door.js +0 -72
|
@@ -2,12 +2,11 @@ import { isRetryableProviderRejection, normalizeModel, observeProviderStatus, st
|
|
|
2
2
|
import { standaloneTenancy, } from "./tenancy.js";
|
|
3
3
|
import { AcceptedSchema } from "@fungi.computer/shiitake/client";
|
|
4
4
|
import { createShiitakeClient } from "@fungi.computer/shiitake/client";
|
|
5
|
-
import {
|
|
5
|
+
import { ShiitakeError } from "@fungi.computer/shiitake";
|
|
6
|
+
import { AgentArchiveRefused, AgentCreateRefused, } from "@fungi.computer/shiitake/agent-control";
|
|
6
7
|
import { AgentDelegation, AgentDisclosureRefused, machineAgentCodecs, } from "@fungi.computer/shiitake/delegation";
|
|
7
8
|
import { FungiMessageMetaSchema } from "@fungi.computer/agar/agents";
|
|
8
9
|
import { agentMail } from "./agent-mail.js";
|
|
9
|
-
import { createChannelConversations, } from "../channels/conversations.js";
|
|
10
|
-
import { channelSessionDoor } from "./channel-session-door.js";
|
|
11
10
|
import { createStandaloneMachineEnrollment, standaloneMachineAgentHome, standaloneMachineLinkAuthority, } from "./machine-link.js";
|
|
12
11
|
import { createMachineLinkFences, MachineEnrollmentRefused, MachineLinkRefused, createMachineAgentClient, } from "../machine-link/index.js";
|
|
13
12
|
import { memberInboxItemSchema } from "@fungi.computer/puffball/inbox";
|
|
@@ -28,6 +27,8 @@ import { homeContract } from "@fungi.computer/shiitake-extensions/home";
|
|
|
28
27
|
import { DurableObject } from "cloudflare:workers";
|
|
29
28
|
import { ShiitakeDO } from "@fungi.computer/shiitake/cloudflare";
|
|
30
29
|
import { agentStoreKeeping } from "./agent-store.js";
|
|
30
|
+
import { agentArchiveOwner } from "./agent-archive-owner.js";
|
|
31
|
+
import { agentArchiveProjection } from "../agent-archive-projection.js";
|
|
31
32
|
import { standaloneBrowserSource } from "./browser.js";
|
|
32
33
|
import { configuredProviderEgress, providerFetch, } from "@fungi.computer/veil/gatekeeper";
|
|
33
34
|
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai/utils/event-stream";
|
|
@@ -38,13 +39,18 @@ import * as Schema from "effect/Schema";
|
|
|
38
39
|
import { z } from "zod";
|
|
39
40
|
import {} from "../cloudflare.js";
|
|
40
41
|
import { AppInstanceError } from "../index.js";
|
|
42
|
+
import { agentScheduleBindingArgs, appAgentScheduleListInputSchema, agentSchedulePageSchema, agentScheduleControlReceiptSchema, } from "../agent-schedules.js";
|
|
43
|
+
import { readAgentSchedulePage, controlAgentSchedule, } from "@fungi.computer/shiitake-extensions/schedules/client";
|
|
44
|
+
import { agentScheduleTargetSchema } from "@fungi.computer/shiitake-extensions/schedules";
|
|
41
45
|
import { createAgentAvatarStore, } from "../agent-avatars.js";
|
|
42
46
|
import { defineIdFamily } from "@fungi.computer/cairn";
|
|
43
47
|
import { agentIds, createAgentDirectory, createFleetSessions, commandSettlementSchema, DirectoryError, } from "@fungi.computer/shiitake/fleet";
|
|
44
48
|
import { appToolPolicySchema } from "../apps/index.js";
|
|
45
|
-
import {
|
|
49
|
+
import { oysterPrincipalId } from "../host.js";
|
|
50
|
+
import { oysterPrincipalIsOwner } from "../authorization.js";
|
|
46
51
|
import { defaultTenant, tenantIdSchema, tenantScope, } from "../tenant.js";
|
|
47
52
|
import { createCodemodeSandbox } from "@fungi.computer/mycelium/workers";
|
|
53
|
+
import { forageDiscoveryInstructions } from "@fungi.computer/mycelium/effect";
|
|
48
54
|
import { maxAgentMcpServers, } from "../agents.js";
|
|
49
55
|
/** Own transport evidence and exact-message decisions for one model lease. */
|
|
50
56
|
function observedModels(model, defaults) {
|
|
@@ -219,6 +225,7 @@ const toMcpServer = (stored) => ({
|
|
|
219
225
|
hasBearer: stored.bearer !== null,
|
|
220
226
|
});
|
|
221
227
|
const instructions = [
|
|
228
|
+
forageDiscoveryInstructions,
|
|
222
229
|
"Attached Computers are independent env.computers.<name> modules, including their Files and execute operations; no App is needed. Discover their exact operations before calling.",
|
|
223
230
|
"You are an agent on a personal Oyster: a small, self-hosted home for Apps.",
|
|
224
231
|
"Your tool is execute: it runs JavaScript in an isolated sandbox. Each installed App is an env.<APP> module; dotted method names are nested properties, e.g. `return await env.BONSAI.repos.list({})`.",
|
|
@@ -229,22 +236,29 @@ const instructions = [
|
|
|
229
236
|
].join("\n");
|
|
230
237
|
/** One agent: Shiitake's Cloudflare host over this Durable Object; its root supplies `hostFor`. */
|
|
231
238
|
export class StandaloneAgentBase extends ShiitakeDO {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
239
|
+
archiveControlOwner;
|
|
240
|
+
archiveControl() {
|
|
241
|
+
return (this.archiveControlOwner ??= agentArchiveOwner({
|
|
242
|
+
storage: this.ctx.storage,
|
|
243
|
+
agentId: () => this.ctx.id.name,
|
|
244
|
+
agent: () => this.shiitake(),
|
|
245
|
+
scheduledExtensions: true,
|
|
246
|
+
refreshOutstanding: (agentId) => this.delegation().refreshOutstanding((target, sessionId, commandId) => this.machineClient().then((client) => client.settlement(target, sessionId, commandId, agentId))),
|
|
247
|
+
record: async (agentId, archived, revision) => {
|
|
248
|
+
const pin = await this.policyDirectory();
|
|
249
|
+
if (pin === null)
|
|
250
|
+
return false;
|
|
251
|
+
return this.env.AGENT_DIRECTORY.getByName(pin.directoryName).recordArchive(agentId, archived, revision);
|
|
252
|
+
},
|
|
253
|
+
wake: () => this.wakeRuntime(),
|
|
254
|
+
waitUntil: (task) => {
|
|
255
|
+
this.ctx.waitUntil(task);
|
|
256
|
+
},
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
/** Owner-local atomic transition. The caller checks human/creator authority. */
|
|
260
|
+
setArchived(archived) {
|
|
261
|
+
return this.archiveControl().setArchived(archived);
|
|
248
262
|
}
|
|
249
263
|
/** Each installed Release's verified catalog surface, built once per Release for this Agent's lifetime. */
|
|
250
264
|
#appSurfaces;
|
|
@@ -389,17 +403,40 @@ export class StandaloneAgentBase extends ShiitakeDO {
|
|
|
389
403
|
}
|
|
390
404
|
}
|
|
391
405
|
controlDirectory() {
|
|
392
|
-
const list = async (signal) => {
|
|
406
|
+
const list = async (signal, options) => {
|
|
393
407
|
signal.throwIfAborted();
|
|
394
408
|
const pin = await this.policyDirectory();
|
|
395
409
|
if (pin === null)
|
|
396
410
|
throw new Error("Agent directory is unavailable");
|
|
397
|
-
const
|
|
398
|
-
|
|
411
|
+
const directory = this.env.AGENT_DIRECTORY.getByName(pin.directoryName);
|
|
412
|
+
const agents = await directory.list(options);
|
|
413
|
+
if (this.ctx.id.name === undefined ||
|
|
414
|
+
!(await directory.has(this.ctx.id.name)))
|
|
399
415
|
throw new Error("Agent is outside this scope");
|
|
400
416
|
signal.throwIfAborted();
|
|
401
417
|
return agents;
|
|
402
418
|
};
|
|
419
|
+
const changeArchive = async (targetId, archived, signal) => {
|
|
420
|
+
await list(signal);
|
|
421
|
+
const caller = this.ctx.id.name;
|
|
422
|
+
const pin = await this.policyDirectory();
|
|
423
|
+
if (caller === undefined || pin === null)
|
|
424
|
+
throw new AgentArchiveRefused("unreadable", "Agent scope is unavailable");
|
|
425
|
+
if (targetId === caller)
|
|
426
|
+
throw new AgentArchiveRefused("self", "An Agent cannot archive or restore itself");
|
|
427
|
+
const directory = this.env.AGENT_DIRECTORY.getByName(pin.directoryName);
|
|
428
|
+
if ((await directory.createdBy(targetId)) !== caller)
|
|
429
|
+
throw new AgentArchiveRefused("not_creator", "An Agent can change only one it created");
|
|
430
|
+
const target = this.env.AGENTS.getByName(targetId);
|
|
431
|
+
const targetPin = await target.policyDirectory();
|
|
432
|
+
if (targetPin?.directoryName !== pin.directoryName ||
|
|
433
|
+
targetPin.tenantId !== pin.tenantId)
|
|
434
|
+
throw new AgentArchiveRefused("unreadable", "Agent archive scope does not match");
|
|
435
|
+
signal.throwIfAborted();
|
|
436
|
+
const result = await target.setArchived(archived);
|
|
437
|
+
if ("error" in result)
|
|
438
|
+
throw new AgentArchiveRefused(result.error, result.message ?? `Agent archive refused: ${result.error}`);
|
|
439
|
+
};
|
|
403
440
|
return {
|
|
404
441
|
list,
|
|
405
442
|
create: async (input, signal) => {
|
|
@@ -420,10 +457,17 @@ export class StandaloneAgentBase extends ShiitakeDO {
|
|
|
420
457
|
},
|
|
421
458
|
send: (input, sessionId, signal, invocation) => this.delegation().send(input, sessionId, signal, invocation),
|
|
422
459
|
recall: (input, sessionId, signal) => this.delegation().recall(input, sessionId, signal),
|
|
460
|
+
archive: (input, signal) => changeArchive(input.agentId, true, signal),
|
|
461
|
+
unarchive: (input, signal) => changeArchive(input.agentId, false, signal),
|
|
423
462
|
open: async (agentId, signal) => {
|
|
424
463
|
const admit = async () => {
|
|
425
|
-
if (!(await list(signal)).some((agent) => agent.agentId === agentId))
|
|
464
|
+
if (!(await list(signal)).some((agent) => agent.agentId === agentId)) {
|
|
465
|
+
const pin = await this.policyDirectory();
|
|
466
|
+
if (pin !== null &&
|
|
467
|
+
(await this.env.AGENT_DIRECTORY.getByName(pin.directoryName).archived(agentId)))
|
|
468
|
+
throw new ShiitakeError("contract", "archived", "Agent is archived");
|
|
426
469
|
throw new Error("Agent is outside this scope");
|
|
470
|
+
}
|
|
427
471
|
};
|
|
428
472
|
await admit();
|
|
429
473
|
const agent = createShiitakeClient({
|
|
@@ -454,7 +498,19 @@ export class StandaloneAgentBase extends ShiitakeDO {
|
|
|
454
498
|
? null
|
|
455
499
|
: { scopeId: pin.directoryName, agentId };
|
|
456
500
|
},
|
|
457
|
-
|
|
501
|
+
// Delegation custody and settlement still see archived identities;
|
|
502
|
+
// Agent-control discovery keeps its active-only default. New sends
|
|
503
|
+
// reach the recipient's admission door and get contract/archived.
|
|
504
|
+
directory: () => {
|
|
505
|
+
const control = this.controlDirectory();
|
|
506
|
+
return {
|
|
507
|
+
...control,
|
|
508
|
+
list: async (signal) => [
|
|
509
|
+
...(await control.list(signal)),
|
|
510
|
+
...(await control.list(signal, { archived: true })),
|
|
511
|
+
],
|
|
512
|
+
};
|
|
513
|
+
},
|
|
458
514
|
deliver: (mail) => this.mail().deliver(mail),
|
|
459
515
|
prepare: () => Promise.resolve(),
|
|
460
516
|
stageAcceptedNotice: (mail, scopeId) => {
|
|
@@ -632,6 +688,28 @@ export class StandaloneAgentBase extends ShiitakeDO {
|
|
|
632
688
|
? null
|
|
633
689
|
: Schema.decodeUnknownSync(policyDirectorySchema)(stored);
|
|
634
690
|
}
|
|
691
|
+
/** Borrow the native owner only after matching its real initialization pin. */
|
|
692
|
+
async scheduleOwner(expected, agentId) {
|
|
693
|
+
const directory = Schema.decodeUnknownSync(policyDirectorySchema)(expected);
|
|
694
|
+
const pin = await this.policyDirectory();
|
|
695
|
+
if (agentId !== this.ctx.id.name ||
|
|
696
|
+
pin?.directoryName !== directory.directoryName ||
|
|
697
|
+
pin.tenantId !== directory.tenantId)
|
|
698
|
+
throw new AppInstanceError("revoked");
|
|
699
|
+
return this.shiitake();
|
|
700
|
+
}
|
|
701
|
+
/** One admitted Agent's bounded native schedule page; this is not a directory scan. */
|
|
702
|
+
async readSchedules(directory, input) {
|
|
703
|
+
const { agentId, ...page } = appAgentScheduleListInputSchema.parse(input);
|
|
704
|
+
const owner = await this.scheduleOwner(directory, agentId);
|
|
705
|
+
return readAgentSchedulePage(agentId, owner, page);
|
|
706
|
+
}
|
|
707
|
+
/** Manage authority returns native identity/state only, never schedule content. */
|
|
708
|
+
async controlSchedule(directory, input, action) {
|
|
709
|
+
const target = agentScheduleTargetSchema.parse(input);
|
|
710
|
+
const owner = await this.scheduleOwner(directory, target.agentId);
|
|
711
|
+
return controlAgentSchedule(owner, target, action);
|
|
712
|
+
}
|
|
635
713
|
/** Listeners from live codemode acquisitions, told when the server list changes. */
|
|
636
714
|
mcpInvalidations = new Set();
|
|
637
715
|
async storedMcpServers() {
|
|
@@ -697,7 +775,13 @@ export class StandaloneAgentBase extends ShiitakeDO {
|
|
|
697
775
|
meta: { schema: FungiMessageMetaSchema },
|
|
698
776
|
...this.mail().host,
|
|
699
777
|
instructions: {
|
|
700
|
-
load: () =>
|
|
778
|
+
load: async () => {
|
|
779
|
+
const { appApprovalInstructions } = await import("../mcp.js");
|
|
780
|
+
return {
|
|
781
|
+
revision: "oyster-7",
|
|
782
|
+
text: instructions + "\n" + appApprovalInstructions,
|
|
783
|
+
};
|
|
784
|
+
},
|
|
701
785
|
},
|
|
702
786
|
tools: {
|
|
703
787
|
sources: [
|
|
@@ -777,7 +861,16 @@ export class StandaloneAgentBase extends ShiitakeDO {
|
|
|
777
861
|
grants: [...extensions.grants, homeGrant],
|
|
778
862
|
},
|
|
779
863
|
...keeping,
|
|
780
|
-
|
|
864
|
+
admission: {
|
|
865
|
+
assertOpen: () => {
|
|
866
|
+
this.archiveControl().assertOpen();
|
|
867
|
+
},
|
|
868
|
+
},
|
|
869
|
+
hostWork: [
|
|
870
|
+
...this.mail().host.hostWork,
|
|
871
|
+
...keeping.hostWork,
|
|
872
|
+
this.archiveControl().work,
|
|
873
|
+
],
|
|
781
874
|
};
|
|
782
875
|
}
|
|
783
876
|
/** Send an installation-admitted message through fleet Session operations. */
|
|
@@ -841,6 +934,27 @@ const visitRow = Schema.Struct({
|
|
|
841
934
|
const counterRow = Schema.Struct({ value: Schema.Number });
|
|
842
935
|
/** Names this Oyster's agents and remembers which Sessions the member opened. */
|
|
843
936
|
export class AgentDirectory extends DurableObject {
|
|
937
|
+
archiveProjectionOwner;
|
|
938
|
+
archiveProjection() {
|
|
939
|
+
return (this.archiveProjectionOwner ??= agentArchiveProjection(this.ctx.storage.sql));
|
|
940
|
+
}
|
|
941
|
+
/** Apply the Agent DO's revisioned fact; an older repair cannot undo a newer one. */
|
|
942
|
+
recordArchive(agentId, archived, revision) {
|
|
943
|
+
const projection = this.archiveProjection();
|
|
944
|
+
return this.ctx.storage.transactionSync(() => {
|
|
945
|
+
if (!this.has(agentId))
|
|
946
|
+
return false;
|
|
947
|
+
const result = projection.record(agentId, archived, revision);
|
|
948
|
+
if (result === "advanced")
|
|
949
|
+
this.bump();
|
|
950
|
+
// A replay after projection but before the Agent's ack still succeeds.
|
|
951
|
+
return result !== "stale";
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
/** Directory's bounded-stale projection, never the Agent's flag authority. */
|
|
955
|
+
archived(agentId) {
|
|
956
|
+
return this.archiveProjection().archived(agentId);
|
|
957
|
+
}
|
|
844
958
|
computers;
|
|
845
959
|
computerNames = new Map();
|
|
846
960
|
directory;
|
|
@@ -1172,10 +1286,14 @@ export class AgentDirectory extends DurableObject {
|
|
|
1172
1286
|
provenance: { actor: oysterPrincipalId(owner), via: captured.agentId },
|
|
1173
1287
|
};
|
|
1174
1288
|
}
|
|
1175
|
-
list() {
|
|
1289
|
+
list(options) {
|
|
1290
|
+
const archived = this.archiveProjection().archivedIds();
|
|
1176
1291
|
return this.directory
|
|
1177
1292
|
.list(this.grant())
|
|
1178
1293
|
.toReversed()
|
|
1294
|
+
.filter(({ agentId }) => options?.archived === true
|
|
1295
|
+
? archived.has(agentId)
|
|
1296
|
+
: !archived.has(agentId))
|
|
1179
1297
|
.map(({ agentId, name, createdBy }) => ({
|
|
1180
1298
|
agentId,
|
|
1181
1299
|
createdBy,
|
|
@@ -1191,6 +1309,13 @@ export class AgentDirectory extends DurableObject {
|
|
|
1191
1309
|
readCreator(agentId) {
|
|
1192
1310
|
return this.directory.readCreator(this.grant(), agentId);
|
|
1193
1311
|
}
|
|
1312
|
+
/** Immutable creating Agent, distinct from the human credential owner. */
|
|
1313
|
+
createdBy(agentId) {
|
|
1314
|
+
const id = agentIds.agent.parse(agentId);
|
|
1315
|
+
return id.ok
|
|
1316
|
+
? (this.directory.read(this.grant(), id.value)?.createdBy ?? null)
|
|
1317
|
+
: null;
|
|
1318
|
+
}
|
|
1194
1319
|
/** Owner-only rename; refuses a name already held by another Agent. */
|
|
1195
1320
|
rename(agentId, name, principal) {
|
|
1196
1321
|
if (!oysterPrincipalIsOwner(principal))
|
|
@@ -1453,7 +1578,19 @@ export function standaloneAgents(env, scope) {
|
|
|
1453
1578
|
sessionOperations: everyOperation,
|
|
1454
1579
|
});
|
|
1455
1580
|
return {
|
|
1456
|
-
list: async () => (await directory().list()).map(toAgent),
|
|
1581
|
+
list: async (options) => (await directory().list(options)).map(toAgent),
|
|
1582
|
+
setArchived: async (agentId, archived, principal) => {
|
|
1583
|
+
if (!oysterPrincipalIsOwner(principal))
|
|
1584
|
+
return { error: "not_granted" };
|
|
1585
|
+
if (!(await directory().has(agentId)))
|
|
1586
|
+
return { error: "not_found" };
|
|
1587
|
+
const agent = env.AGENTS.getByName(agentId);
|
|
1588
|
+
const pin = await agent.policyDirectory();
|
|
1589
|
+
if (pin?.directoryName !== directoryName ||
|
|
1590
|
+
pin.tenantId !== (scope?.tenantId ?? defaultTenant))
|
|
1591
|
+
return { error: "not_found" };
|
|
1592
|
+
return agent.setArchived(archived);
|
|
1593
|
+
},
|
|
1457
1594
|
create: async (name, requestId, principal, actor, avatar) => {
|
|
1458
1595
|
const entry = await directory().create(name, requestId, principal, actor, avatar);
|
|
1459
1596
|
await env.AGENTS.getByName(entry.agentId).usePolicyDirectory({
|
|
@@ -1624,10 +1761,58 @@ export const agentFrameMethods = {
|
|
|
1624
1761
|
},
|
|
1625
1762
|
},
|
|
1626
1763
|
AGENTS: {
|
|
1764
|
+
schedules: {
|
|
1765
|
+
decodeArgs: (raw) => agentScheduleBindingArgs.schedules.parse(raw),
|
|
1766
|
+
decodeResult: Schema.decodeUnknownSync(agentSchedulePageSchema, {
|
|
1767
|
+
onExcessProperty: "error",
|
|
1768
|
+
}),
|
|
1769
|
+
requiredOperations: () => ["schedules"],
|
|
1770
|
+
requestedResource: (raw) => ({
|
|
1771
|
+
kind: "agent",
|
|
1772
|
+
id: agentScheduleBindingArgs.schedules.parse(raw)[0].agentId,
|
|
1773
|
+
}),
|
|
1774
|
+
},
|
|
1775
|
+
pauseSchedule: {
|
|
1776
|
+
decodeArgs: (raw) => agentScheduleBindingArgs.pauseSchedule.parse(raw),
|
|
1777
|
+
decodeResult: Schema.decodeUnknownSync(agentScheduleControlReceiptSchema, {
|
|
1778
|
+
onExcessProperty: "error",
|
|
1779
|
+
}),
|
|
1780
|
+
requiredOperations: () => ["pauseSchedule"],
|
|
1781
|
+
requestedResource: (raw) => ({
|
|
1782
|
+
kind: "agent",
|
|
1783
|
+
id: agentScheduleBindingArgs.pauseSchedule.parse(raw)[0].agentId,
|
|
1784
|
+
}),
|
|
1785
|
+
},
|
|
1786
|
+
resumeSchedule: {
|
|
1787
|
+
decodeArgs: (raw) => agentScheduleBindingArgs.resumeSchedule.parse(raw),
|
|
1788
|
+
decodeResult: Schema.decodeUnknownSync(agentScheduleControlReceiptSchema, {
|
|
1789
|
+
onExcessProperty: "error",
|
|
1790
|
+
}),
|
|
1791
|
+
requiredOperations: () => ["resumeSchedule"],
|
|
1792
|
+
requestedResource: (raw) => ({
|
|
1793
|
+
kind: "agent",
|
|
1794
|
+
id: agentScheduleBindingArgs.resumeSchedule.parse(raw)[0].agentId,
|
|
1795
|
+
}),
|
|
1796
|
+
},
|
|
1797
|
+
deleteSchedule: {
|
|
1798
|
+
decodeArgs: (raw) => agentScheduleBindingArgs.deleteSchedule.parse(raw),
|
|
1799
|
+
decodeResult: Schema.decodeUnknownSync(agentScheduleControlReceiptSchema, {
|
|
1800
|
+
onExcessProperty: "error",
|
|
1801
|
+
}),
|
|
1802
|
+
requiredOperations: () => ["deleteSchedule"],
|
|
1803
|
+
requestedResource: (raw) => ({
|
|
1804
|
+
kind: "agent",
|
|
1805
|
+
id: agentScheduleBindingArgs.deleteSchedule.parse(raw)[0].agentId,
|
|
1806
|
+
}),
|
|
1807
|
+
},
|
|
1627
1808
|
send: {
|
|
1628
1809
|
decodeArgs: (raw) => agentBindingArgs.AGENTS.send.parse(raw),
|
|
1629
1810
|
decodeResult: decodeAgentAccepted,
|
|
1630
1811
|
requiredOperations: () => ["send"],
|
|
1812
|
+
requestedResource: (raw) => ({
|
|
1813
|
+
kind: "agent",
|
|
1814
|
+
id: agentBindingArgs.AGENTS.send.parse(raw)[0].agentId,
|
|
1815
|
+
}),
|
|
1631
1816
|
},
|
|
1632
1817
|
},
|
|
1633
1818
|
};
|
|
@@ -1669,9 +1854,45 @@ export async function dispatchAgentBinding(env, scope, frame, props, method, arg
|
|
|
1669
1854
|
});
|
|
1670
1855
|
result = { status: accepted.status, commandId: accepted.commandId };
|
|
1671
1856
|
}
|
|
1857
|
+
else if (props.binding === "AGENTS" &&
|
|
1858
|
+
(method === "schedules" ||
|
|
1859
|
+
method === "pauseSchedule" ||
|
|
1860
|
+
method === "resumeSchedule" ||
|
|
1861
|
+
method === "deleteSchedule")) {
|
|
1862
|
+
const [input] = agentScheduleBindingArgs[method].parse(args);
|
|
1863
|
+
if (!(await directory.has(input.agentId)))
|
|
1864
|
+
throw new Error("No such agent");
|
|
1865
|
+
const home = await directory.agentHome(input.agentId);
|
|
1866
|
+
if (home?.kind === "machine")
|
|
1867
|
+
throw new MachineLinkRefused("actor_not_allowed");
|
|
1868
|
+
if (home === null)
|
|
1869
|
+
throw new AppInstanceError("revoked");
|
|
1870
|
+
frame.signal.throwIfAborted();
|
|
1871
|
+
const agent = env.AGENTS.getByName(input.agentId);
|
|
1872
|
+
const pin = {
|
|
1873
|
+
directoryName: scope.durableName("agents"),
|
|
1874
|
+
tenantId: scope.tenantId,
|
|
1875
|
+
};
|
|
1876
|
+
if (method === "schedules") {
|
|
1877
|
+
result = await agent.readSchedules(pin, agentScheduleBindingArgs.schedules.parse(args)[0]);
|
|
1878
|
+
}
|
|
1879
|
+
else {
|
|
1880
|
+
result = await agent.controlSchedule(pin, agentScheduleBindingArgs[method].parse(args)[0], method === "pauseSchedule"
|
|
1881
|
+
? "pause"
|
|
1882
|
+
: method === "resumeSchedule"
|
|
1883
|
+
? "resume"
|
|
1884
|
+
: "delete");
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1672
1887
|
else {
|
|
1673
1888
|
throw new AppInstanceError("revoked");
|
|
1674
1889
|
}
|
|
1675
1890
|
frame.signal.throwIfAborted();
|
|
1676
|
-
return Schema.decodeUnknownSync(Schema.Union([
|
|
1891
|
+
return Schema.decodeUnknownSync(Schema.Union([
|
|
1892
|
+
inboxPageSchema,
|
|
1893
|
+
inboxVisitSchema,
|
|
1894
|
+
AcceptedSchema,
|
|
1895
|
+
agentSchedulePageSchema,
|
|
1896
|
+
agentScheduleControlReceiptSchema,
|
|
1897
|
+
]), { onExcessProperty: "error" })(result);
|
|
1677
1898
|
}
|
|
@@ -9,7 +9,7 @@ import { z } from "zod";
|
|
|
9
9
|
import { buildStartFailureReply } from "./builds.js";
|
|
10
10
|
import * as Schema from "effect/Schema";
|
|
11
11
|
import { AppInstanceError } from "../index.js";
|
|
12
|
-
import { installOrSelect, verifyRelease
|
|
12
|
+
import { installOrSelect, verifyRelease } from "../host.js";
|
|
13
13
|
import { repositoryWorkflowSourceSchema } from "@fungi.computer/workflows-wire";
|
|
14
14
|
import { installationIdSchema } from "../apps/index.js";
|
|
15
15
|
import { appIdSchema, releaseSchema } from "@fungi.computer/puffball/effect";
|
|
@@ -9,8 +9,7 @@ import { publishSource } from "../pack.js";
|
|
|
9
9
|
import { appBuildRecipe, readBuildManifest } from "../build/workspace-build.js";
|
|
10
10
|
import { parseSourceRevision } from "@fungi.computer/puffball";
|
|
11
11
|
import { releaseSchema } from "@fungi.computer/puffball/effect";
|
|
12
|
-
import {
|
|
13
|
-
import { appPermissionPageSchema } from "../apps/permissions.js";
|
|
12
|
+
import { approveChosenDashboard } from "./dashboard-approval.js";
|
|
14
13
|
import { installOnOyster, InstallationObservationError, } from "../installation-client.js";
|
|
15
14
|
export const standaloneAppSet = [
|
|
16
15
|
{
|
|
@@ -141,57 +140,6 @@ export async function installStandaloneAppSet(options) {
|
|
|
141
140
|
init.signal = signal;
|
|
142
141
|
return fetch(new URL(path, options.url), init);
|
|
143
142
|
};
|
|
144
|
-
const approveChosenDashboard = async (installed, generation) => {
|
|
145
|
-
const name = installed.installationId;
|
|
146
|
-
const path = `/api/installations/${encodeURIComponent(name)}/grants`;
|
|
147
|
-
const projectionResponse = await owner("GET", path);
|
|
148
|
-
if (!projectionResponse.ok)
|
|
149
|
-
throw new Error(`${name} approval read failed (${String(projectionResponse.status)})`);
|
|
150
|
-
const projection = appPermissionPageSchema.parse(await projectionResponse.json());
|
|
151
|
-
const { permissions } = projection;
|
|
152
|
-
const digest = installed.release.artifact.artifactDigest;
|
|
153
|
-
if (projection.storeGeneration !== generation ||
|
|
154
|
-
projection.installationId !== name ||
|
|
155
|
-
projection.artifactDigest !== digest ||
|
|
156
|
-
permissions.declaredOperations.artifactDigest !== digest ||
|
|
157
|
-
permissions.authority.authorityVersion !==
|
|
158
|
-
installed.authority.authorityVersion ||
|
|
159
|
-
permissions.authority.declaredOperationsDigest !==
|
|
160
|
-
installed.authority.declaredOperationsDigest ||
|
|
161
|
-
permissions.declaredOperations.digest !==
|
|
162
|
-
installed.authority.declaredOperationsDigest)
|
|
163
|
-
throw new Error(`${name} approval snapshot changed`);
|
|
164
|
-
const shell = permissions.declaredOperations.capabilities.find((row) => row.capability === "team.workbenchHost");
|
|
165
|
-
if (shell === undefined)
|
|
166
|
-
return;
|
|
167
|
-
const command = grantCommandSchema.parse({
|
|
168
|
-
kind: "change-grants",
|
|
169
|
-
changes: [
|
|
170
|
-
{
|
|
171
|
-
capability: shell.capability,
|
|
172
|
-
grant: { operations: shell.operations, resources: "tenant-scope" },
|
|
173
|
-
},
|
|
174
|
-
],
|
|
175
|
-
});
|
|
176
|
-
const saved = await owner("PUT", path, Schema.decodeUnknownSync(Schema.Json)({
|
|
177
|
-
storeGeneration: projection.storeGeneration,
|
|
178
|
-
authorityVersion: installed.authority.authorityVersion,
|
|
179
|
-
artifactDigest: digest,
|
|
180
|
-
command,
|
|
181
|
-
}));
|
|
182
|
-
if (!saved.ok)
|
|
183
|
-
throw new Error(`${name} approval failed (${String(saved.status)})`);
|
|
184
|
-
const receipt = installationSchema.parse(await saved.json());
|
|
185
|
-
if (receipt.installationId !== name ||
|
|
186
|
-
receipt.instanceId !== installed.instanceId ||
|
|
187
|
-
receipt.release.artifact.artifactDigest !== digest ||
|
|
188
|
-
receipt.authority.authorityVersion !==
|
|
189
|
-
installed.authority.authorityVersion + 1 ||
|
|
190
|
-
!receipt.authority.approvedGrants.some((grant) => grant.capability === "team.workbenchHost" &&
|
|
191
|
-
grant.resources === "tenant-scope" &&
|
|
192
|
-
shell.operations.every((operation) => grant.operations.includes(operation))))
|
|
193
|
-
throw new Error(`${name} approval incomplete`);
|
|
194
|
-
};
|
|
195
143
|
const idle = options.idle ??
|
|
196
144
|
((ms, signal) => sleep(ms, undefined, { signal }));
|
|
197
145
|
/** Start one build and wait for its terminal outcome within the build
|
|
@@ -392,7 +340,7 @@ export async function installStandaloneAppSet(options) {
|
|
|
392
340
|
if (record.status !== "enabled")
|
|
393
341
|
throw new Error(`${name} fresh installation is not enabled`);
|
|
394
342
|
if (next.app.dashboard === true)
|
|
395
|
-
await approveChosenDashboard(
|
|
343
|
+
await approveChosenDashboard(operation, owner);
|
|
396
344
|
}
|
|
397
345
|
options.report(`${operation.creation === "replacement" ? "Replaced" : "Installed"} ${name} from its build ${build.jobId}`);
|
|
398
346
|
// Report-only UI check: never blocks, but the owner sees it here.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type Routes } from "../authorization.js";
|
|
2
2
|
import type { TenantScope } from "../tenant.js";
|
|
3
3
|
import type { OysterBindingResult } from "../workers.js";
|
|
4
4
|
import type { OysterEnv } from "../host.js";
|
|
@@ -34,8 +34,7 @@ export declare const appSourceFrameMethods: {
|
|
|
34
34
|
* re-checked here (never trusted from a stale captured frame). */
|
|
35
35
|
export declare function dispatchAppSourceBinding(env: AppSourceBindingEnv, scope: TenantScope, frame: InvocationFrame, method: string, args: readonly unknown[]): Promise<OysterBindingResult>;
|
|
36
36
|
/** Operator-only: seed one App's editable source template (`install-apps.mjs`).
|
|
37
|
-
*
|
|
38
|
-
* path before this handler runs (`host.ts`'s `oysterPrincipalIsOwner` check).
|
|
37
|
+
* Each route declares the owner requirement (`authorization.ts`).
|
|
39
38
|
* The operator `git push`es the projected source to
|
|
40
39
|
* `/api/apps/:appId/source.git`, then claims that head here. */
|
|
41
|
-
export declare function appSourceOwnerRoutes(env: AppSourceBindingEnv, scope: TenantScope):
|
|
40
|
+
export declare function appSourceOwnerRoutes(env: AppSourceBindingEnv, scope: TenantScope): Routes<OysterEnv>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { owner, routes as routeTable } from "../authorization.js";
|
|
2
2
|
import { bodyLimit } from "hono/body-limit";
|
|
3
3
|
import * as Schema from "effect/Schema";
|
|
4
4
|
import { AppInstanceError } from "../index.js";
|
|
@@ -32,18 +32,14 @@ export async function dispatchAppSourceBinding(env, scope, frame, method, args)
|
|
|
32
32
|
return Schema.decodeUnknownSync(appForkResultSchema)(result);
|
|
33
33
|
}
|
|
34
34
|
/** Operator-only: seed one App's editable source template (`install-apps.mjs`).
|
|
35
|
-
*
|
|
36
|
-
* path before this handler runs (`host.ts`'s `oysterPrincipalIsOwner` check).
|
|
35
|
+
* Each route declares the owner requirement (`authorization.ts`).
|
|
37
36
|
* The operator `git push`es the projected source to
|
|
38
37
|
* `/api/apps/:appId/source.git`, then claims that head here. */
|
|
39
38
|
export function appSourceOwnerRoutes(env, scope) {
|
|
40
|
-
const routes =
|
|
39
|
+
const routes = routeTable();
|
|
41
40
|
const registry = () => env.REGISTRY.getByName(scope.durableName("registry"));
|
|
42
|
-
routes.on(["GET", "POST"], "/api/apps/:appId/source.git/:suffix{info/refs|git-upload-pack|git-receive-pack}", (context) => registry().exchangeTemplateSource(context.req.param("appId"), context.req.param("suffix"), context.req.raw));
|
|
43
|
-
routes.put("/api/apps/:appId/source",
|
|
44
|
-
maxSize: TEMPLATE_SEED_CLAIM_MAX_BYTES,
|
|
45
|
-
onError: (context) => context.json({ kind: "invalid" }, 413),
|
|
46
|
-
}), async (context) => {
|
|
41
|
+
routes.on(["GET", "POST"], "/api/apps/:appId/source.git/:suffix{info/refs|git-upload-pack|git-receive-pack}", owner("direct-or-app"), (context) => registry().exchangeTemplateSource(context.req.param("appId"), context.req.param("suffix"), context.req.raw));
|
|
42
|
+
routes.put("/api/apps/:appId/source", owner("direct-or-app"), async (context) => {
|
|
47
43
|
const claim = parseTemplateSeedClaim(await context.req.text());
|
|
48
44
|
if (claim === null)
|
|
49
45
|
return context.json({ kind: "invalid" }, 400);
|
|
@@ -54,6 +50,13 @@ export function appSourceOwnerRoutes(env, scope) {
|
|
|
54
50
|
? 409
|
|
55
51
|
: 503;
|
|
56
52
|
return Response.json(result, { status });
|
|
53
|
+
}, {
|
|
54
|
+
middleware: [
|
|
55
|
+
bodyLimit({
|
|
56
|
+
maxSize: TEMPLATE_SEED_CLAIM_MAX_BYTES,
|
|
57
|
+
onError: (context) => context.json({ kind: "invalid" }, 413),
|
|
58
|
+
}),
|
|
59
|
+
],
|
|
57
60
|
});
|
|
58
61
|
return routes;
|
|
59
62
|
}
|
|
@@ -1,18 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Owner routes for App builds on the standalone's Computer. They run after
|
|
3
|
-
* the host's owner guard; the build owner is the registry.
|
|
4
|
-
*
|
|
5
|
-
* Repository Apps register through APPS.register from their committed root
|
|
6
|
-
* metadata. Publishing an App's source (PUT /api/apps/:appId/source) instead
|
|
7
|
-
* makes its template the build source at the returned seed revision.
|
|
8
|
-
*
|
|
9
|
-
* - `POST /api/builds` starts a build of that source at one exact commit.
|
|
10
|
-
* - `GET /api/builds/:jobId` reads it; a finished build carries its Release,
|
|
11
|
-
* which the owner installs or selects through the installation routes.
|
|
12
|
-
*/
|
|
13
|
-
import { Hono } from "hono";
|
|
14
1
|
import { type OysterBuildStartReason } from "../app-builds.js";
|
|
15
2
|
import { type OysterEnv } from "../host.js";
|
|
3
|
+
import { type Routes } from "../authorization.js";
|
|
16
4
|
import type { StandaloneRegistry } from "./registry.js";
|
|
17
5
|
/** Both HTTP and APPS expose the same closed preflight reason. */
|
|
18
6
|
export declare function buildStartFailureReply(reason: OysterBuildStartReason | "unavailable"): {
|
|
@@ -24,4 +12,4 @@ export declare function buildStartFailureReply(reason: OysterBuildStartReason |
|
|
|
24
12
|
};
|
|
25
13
|
/** The build routes over the tenant registry that owns builds, resolved per
|
|
26
14
|
* request so mounting the routes reaches no registry. */
|
|
27
|
-
export declare function standaloneBuildRoutes(registryFor: () =>
|
|
15
|
+
export declare function standaloneBuildRoutes(registryFor: () => Pick<StandaloneRegistry, "startBuild" | "readBuild" | "readBuildLog">): Routes<OysterEnv>;
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
* makes its template the build source at the returned seed revision.
|
|
8
8
|
*
|
|
9
9
|
* - `POST /api/builds` starts a build of that source at one exact commit.
|
|
10
|
-
* - `GET /api/builds/:jobId` reads it;
|
|
11
|
-
*
|
|
10
|
+
* - `GET /api/builds/:jobId` reads it; `?log` reads its combined raw output.
|
|
11
|
+
* Both require the same owner admission. A finished build carries its
|
|
12
|
+
* Release, which the owner installs or selects through the installation routes.
|
|
12
13
|
*/
|
|
13
|
-
import { Hono } from "hono";
|
|
14
14
|
import * as Schema from "effect/Schema";
|
|
15
15
|
import { oysterBuildStartMessages, } from "../app-builds.js";
|
|
16
|
-
import {
|
|
16
|
+
import {} from "../host.js";
|
|
17
|
+
import { owner as ownerRoute, routes as routeTable, } from "../authorization.js";
|
|
17
18
|
import { appIdSchema, releaseSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
|
|
18
19
|
const buildInput = Schema.Struct({
|
|
19
20
|
appId: appIdSchema,
|
|
@@ -38,14 +39,9 @@ export function buildStartFailureReply(reason) {
|
|
|
38
39
|
/** The build routes over the tenant registry that owns builds, resolved per
|
|
39
40
|
* request so mounting the routes reaches no registry. */
|
|
40
41
|
export function standaloneBuildRoutes(registryFor) {
|
|
41
|
-
const routes =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
: Response.json({ error: "Owner only" }, { status: 403 }));
|
|
45
|
-
routes.post("/api/builds", async (context) => {
|
|
46
|
-
const principal = context.get("principal");
|
|
47
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
48
|
-
return Response.json({ error: "Owner only" }, { status: 403 });
|
|
42
|
+
const routes = routeTable();
|
|
43
|
+
const owner = ownerRoute("direct-or-app");
|
|
44
|
+
routes.post("/api/builds", owner, async (context, { principal }) => {
|
|
49
45
|
let input;
|
|
50
46
|
try {
|
|
51
47
|
input = Schema.decodeUnknownSync(buildInput, {
|
|
@@ -70,7 +66,13 @@ export function standaloneBuildRoutes(registryFor) {
|
|
|
70
66
|
return Response.json(reply.body, { status: reply.status });
|
|
71
67
|
}
|
|
72
68
|
});
|
|
73
|
-
routes.get("/api/builds/:jobId", async (context) => {
|
|
69
|
+
routes.get("/api/builds/:jobId", owner, async (context) => {
|
|
70
|
+
if (context.req.query("log") !== undefined) {
|
|
71
|
+
const log = await registryFor().readBuildLog(context.req.param("jobId"));
|
|
72
|
+
return log === null
|
|
73
|
+
? Response.json({ error: "Not found" }, { status: 404 })
|
|
74
|
+
: Response.json(log);
|
|
75
|
+
}
|
|
74
76
|
const build = await registryFor().readBuild(context.req.param("jobId"));
|
|
75
77
|
return build === null
|
|
76
78
|
? Response.json({ error: "Not found" }, { status: 404 })
|