@fungi.computer/oyster 0.0.1 → 0.2.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 +228 -176
- package/dist/agent-avatars.d.ts +103 -0
- package/dist/agent-avatars.js +66 -0
- package/dist/agents.d.ts +12 -2
- package/dist/agents.js +39 -2
- package/dist/app-builds.d.ts +76 -17
- package/dist/app-builds.js +207 -63
- package/dist/app-sources.d.ts +7 -22
- package/dist/app-sources.js +49 -14
- package/dist/apps/gate.d.ts +7 -7
- package/dist/apps/gate.js +6 -6
- package/dist/apps/installation.d.ts +35 -35
- package/dist/apps/installation.js +20 -19
- package/dist/build/artifact-writer.d.ts +40 -0
- package/dist/build/artifact-writer.js +181 -0
- package/dist/build/build-jobs.js +7 -1
- package/dist/build/build-policy.d.ts +10 -0
- package/dist/build/build-policy.js +10 -0
- package/dist/build/build-step.d.ts +12 -1
- package/dist/build/build-step.js +9 -1
- package/dist/build/candidate-builder.d.ts +68 -0
- package/dist/build/candidate-builder.js +91 -0
- package/dist/build/candidate-cli.d.ts +1 -0
- package/dist/build/candidate-cli.js +119 -0
- package/dist/build/candidate-cli.mjs +28220 -0
- package/dist/build/content-objects.d.ts +37 -0
- package/dist/build/content-objects.js +53 -0
- package/dist/build/effect.d.ts +18 -0
- package/dist/build/effect.js +52 -12
- package/dist/build/git-environment.d.ts +3 -0
- package/dist/build/git-environment.js +21 -9
- package/dist/build/local-build.d.ts +13 -3
- package/dist/build/local-build.js +149 -99
- package/dist/build/snapshot-package-store.d.ts +10 -0
- package/dist/build/snapshot-package-store.js +15 -0
- package/dist/build/snapshot-program-scripts.js +1 -1
- package/dist/build/snapshot-programs/snapshot-path.d.ts +2 -0
- package/dist/build/snapshot-programs/snapshot-path.js +6 -0
- package/dist/build/submission-contract.d.ts +461 -17
- package/dist/build/submission-contract.js +136 -26
- package/dist/build/workspace-build.d.ts +7 -2
- package/dist/build/workspace-build.js +55 -11
- package/dist/build-contract.d.ts +3 -0
- package/dist/build-contract.js +3 -0
- package/dist/build-egress.d.ts +62 -0
- package/dist/build-egress.js +148 -0
- package/dist/build.d.ts +1 -0
- package/dist/build.js +1 -0
- package/dist/cloudflare.js +4 -4
- package/dist/frame-binding-wire-script.js +1 -1
- package/dist/frame-binding-wire.d.ts +1 -1
- package/dist/frame-binding-wire.js +1 -1
- package/dist/git-journal.js +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/installation-consent.d.ts +3 -1
- package/dist/installation-consent.js +2 -0
- package/dist/mcp-owner.js +20 -8
- package/dist/pack.d.ts +6 -3
- package/dist/pack.js +28 -11
- package/dist/page/gill-observation.d.ts +4 -0
- package/dist/page/gill-observation.js +31 -0
- package/dist/page/mount.d.ts +4 -8
- package/dist/page/window-launch.d.ts +18 -0
- package/dist/page/window-launch.js +11 -0
- package/dist/page-script.js +4 -4
- package/dist/release.d.ts +11 -0
- package/dist/release.js +4 -1
- package/dist/shell/app-directory.d.ts +50 -45
- package/dist/shell/app-directory.js +19 -18
- package/dist/shell/shell-module.d.ts +26 -2
- package/dist/shell/shell-module.js +51 -2
- package/dist/shell/window-manager.d.ts +5 -1
- package/dist/shell/window-manager.js +21 -2
- package/dist/shell/workbench-directory.d.ts +332 -0
- package/dist/shell/workbench-directory.js +35 -0
- package/dist/shell/workbench-host-types.d.ts +1 -1
- package/dist/shell/workbench-host.js +5 -0
- package/dist/shell/workbench-model.d.ts +3 -1
- package/dist/source/index.d.ts +6 -7
- package/dist/source/index.js +6 -7
- package/dist/source/template-push.d.ts +33 -0
- package/dist/source/template-push.js +223 -0
- package/dist/source/template-repository.d.ts +2 -0
- package/dist/source/template-repository.js +13 -0
- package/dist/source/{template-write.d.ts → template-seed.d.ts} +19 -20
- package/dist/source/template-seed.js +80 -0
- package/dist/standalone/agent-directory.d.ts +4 -0
- package/dist/standalone/agent-directory.js +4 -0
- package/dist/standalone/agents.d.ts +227 -0
- package/dist/standalone/agents.js +834 -0
- package/dist/standalone/app-builds-binding.d.ts +36 -0
- package/dist/standalone/app-builds-binding.js +119 -0
- package/dist/standalone/app-set.d.ts +91 -0
- package/dist/standalone/app-set.js +395 -0
- package/dist/standalone/app-source.d.ts +41 -0
- package/dist/standalone/app-source.js +59 -0
- package/dist/standalone/builds.d.ts +18 -0
- package/dist/standalone/builds.js +101 -0
- package/dist/standalone/computer-egress.d.ts +37 -0
- package/dist/standalone/computer-egress.js +111 -0
- package/dist/standalone/computer.d.ts +122 -0
- package/dist/standalone/computer.js +714 -0
- package/dist/standalone/install-apps-cli.d.ts +2 -0
- package/dist/standalone/install-apps-cli.js +126 -0
- package/dist/standalone/instance.d.ts +35 -0
- package/dist/standalone/instance.js +74 -0
- package/dist/standalone/media.d.ts +76 -0
- package/dist/standalone/media.js +227 -0
- package/dist/standalone/preview.d.ts +51 -0
- package/dist/standalone/preview.js +167 -0
- package/dist/standalone/registry.d.ts +102 -0
- package/dist/standalone/registry.js +193 -0
- package/dist/standalone/tenancy.d.ts +42 -0
- package/dist/standalone/tenancy.js +241 -0
- package/dist/standalone/worker.d.ts +55 -0
- package/dist/standalone/worker.js +147 -0
- package/dist/standalone/workflow-authority-readers.d.ts +16 -0
- package/dist/standalone/workflow-authority-readers.js +35 -0
- package/dist/standalone/workflow-binding.d.ts +216 -0
- package/dist/standalone/workflow-binding.js +217 -0
- package/dist/standalone/workflow-events.d.ts +46 -0
- package/dist/standalone/workflow-events.js +39 -0
- package/dist/standalone/workflow-git-capture.d.ts +7 -0
- package/dist/standalone/workflow-git-capture.js +62 -0
- package/dist/standalone/workflow-host.d.ts +955 -0
- package/dist/standalone/workflow-host.js +778 -0
- package/dist/standalone/workflow-node-capture.d.ts +13 -0
- package/dist/standalone/workflow-node-capture.js +72 -0
- package/dist/standalone/workflow-root-authority.d.ts +27 -0
- package/dist/standalone/workflow-root-authority.js +87 -0
- package/dist/standalone/workflow-schedules.d.ts +52 -0
- package/dist/standalone/workflow-schedules.js +40 -0
- package/dist/standalone/workflow-trigger-host.d.ts +1716 -0
- package/dist/standalone/workflow-trigger-host.js +52 -0
- package/dist/standalone/workflows.d.ts +713 -0
- package/dist/standalone/workflows.js +554 -0
- package/dist/tenancy/auth0.d.ts +64 -0
- package/dist/tenancy/auth0.js +198 -0
- package/dist/tenancy/cloudflare/rate-limit.d.ts +8 -0
- package/dist/tenancy/cloudflare/rate-limit.js +19 -0
- package/dist/tenancy/cloudflare/registry.d.ts +50 -0
- package/dist/tenancy/cloudflare/registry.js +301 -0
- package/dist/tenancy/cloudflare.d.ts +2 -0
- package/dist/tenancy/cloudflare.js +2 -0
- package/dist/tenancy/cold-directory.d.ts +13 -0
- package/dist/tenancy/cold-directory.js +43 -0
- package/dist/tenancy/contracts.d.ts +195 -0
- package/dist/tenancy/contracts.js +1 -0
- package/dist/tenancy/index.d.ts +19 -0
- package/dist/tenancy/index.js +156 -0
- package/dist/tenancy/owner-key.d.ts +26 -0
- package/dist/tenancy/owner-key.js +63 -0
- package/dist/tenancy/oyster.d.ts +4 -0
- package/dist/tenancy/oyster.js +25 -0
- package/dist/tenancy/registry-record.d.ts +7 -0
- package/dist/tenancy/registry-record.js +44 -0
- package/dist/tenancy/registry.d.ts +27 -0
- package/dist/tenancy/registry.js +138 -0
- package/dist/tenancy/resolve.d.ts +7 -0
- package/dist/tenancy/resolve.js +64 -0
- package/dist/workers-git.js +1 -4
- package/dist/workers.d.ts +2 -2
- package/dist/workers.js +3 -1
- package/docs/api/build-contract.md +829 -32
- package/docs/api/build.md +253 -17
- package/docs/api/modules.md +40 -32
- package/docs/api/pack.md +6 -3
- package/docs/api/shell/app-directory.md +267 -0
- package/docs/api/shell/shell-module.md +64 -3
- package/docs/api/shell/workbench-directory.md +119 -0
- package/docs/api/shell/workbench.md +19 -0
- package/docs/api/shell.md +127 -0
- package/docs/api/tenancy/auth0.md +110 -0
- package/docs/api/tenancy/cloudflare.md +221 -0
- package/docs/api/tenancy/cold-directory.md +46 -0
- package/docs/api/tenancy/owner-key.md +63 -0
- package/docs/api/tenancy/registry.md +77 -0
- package/docs/api/tenancy/resolve.md +69 -0
- package/docs/api/tenancy.md +430 -0
- package/docs/api/workers-git.md +140 -21
- package/docs/api/workers.md +15 -2
- package/docs/site/agent-quickstart.mdx +52 -55
- package/examples/standalone/worker.ts +7 -14
- package/examples/standalone/wrangler.jsonc +67 -6
- package/package.json +78 -25
- package/dist/build/r2-artifact-writer.d.ts +0 -31
- package/dist/build/r2-artifact-writer.js +0 -179
- package/dist/source/template-write.js +0 -344
- package/docs/media/architecture.mmd +0 -32
- package/docs/media/architecture.png +0 -0
- package/docs/media/bonsai-network.png +0 -0
- package/docs/media/bonsai-repository.png +0 -0
- package/examples/git/worker.ts +0 -39
- package/examples/git/wrangler.jsonc +0 -32
package/dist/app-builds.js
CHANGED
|
@@ -5,43 +5,72 @@
|
|
|
5
5
|
* A build runs the shared job lifecycle (`build/build-jobs.ts`) and the fixed
|
|
6
6
|
* captured builder command on one Computer. The Computer holds no key: its
|
|
7
7
|
* clone is admitted at egress for exactly the job's one repository (read
|
|
8
|
-
* only), and its candidate upload for exactly the job.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* only), and its candidate upload for exactly the job. A source is either a
|
|
9
|
+
* repository in an installation's Git (a fork, through the Git door) or the
|
|
10
|
+
* App's own published template, built only at a recorded seed revision of
|
|
11
|
+
* its pinned recipe (no installed App is needed). The build streams each
|
|
12
|
+
* built object into the host's artifact store itself, through the egress, and
|
|
13
|
+
* then submits a small manifest naming them. This owner only records facts:
|
|
14
|
+
* it checks each named object's stored size and digest, writes the asset
|
|
15
|
+
* manifest and App root in Oyster's one immutable artifact representation
|
|
16
|
+
* (the same objects a packed upload writes), verifies the Release like any
|
|
17
|
+
* installed one, and records it; installing it is a separate owner act.
|
|
11
18
|
*/
|
|
12
|
-
import * as Effect from "effect/Effect";
|
|
13
19
|
import * as Schema from "effect/Schema";
|
|
14
20
|
import { parseArtifactRef, parseReleaseId, } from "@fungi.computer/puffball";
|
|
15
21
|
import { appIdSchema, releaseSchema, sha256DigestSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
|
|
16
|
-
import { capturedPinnedRecipeSchema,
|
|
17
|
-
import {
|
|
22
|
+
import { capturedPinnedRecipeSchema, digestPinnedRecipe, fitsPinnedRecipe, pinnedRecipeObjectBound, } from "./build/effect.js";
|
|
23
|
+
import { candidateManifestObjects, decodeCandidateBackend, digestTeamAppCandidateManifest, } from "./build/submission-contract.js";
|
|
24
|
+
import { contentObjectRef } from "./build/content-objects.js";
|
|
18
25
|
import { createTeamBuildPublisher, teamAppBuildCommand, } from "./build/build-jobs.js";
|
|
19
|
-
import {
|
|
26
|
+
import { writeArtifactRoot, } from "./build/artifact-writer.js";
|
|
20
27
|
import { buildGitPassPrefix } from "./git-auth.js";
|
|
21
|
-
import { verifyRelease } from "./release.js";
|
|
22
|
-
|
|
28
|
+
import { defaultAssetPolicy, readVerified, verifyRelease, } from "./release.js";
|
|
29
|
+
import { appForTemplateRepository, templateRepositoryForApp, } from "./source/template-repository.js";
|
|
30
|
+
/** A Computer's whole-build budget: a cold build on a 6 GiB Computer measured 185-270 s. */
|
|
23
31
|
export const oysterAppBuildTimeoutMs = 900_000;
|
|
24
32
|
/** The job, its clone admission and its upload grant end together. */
|
|
25
33
|
const jobTtlMs = oysterAppBuildTimeoutMs + 5 * 60_000;
|
|
26
34
|
/** Host names the Computer's egress serves for a build; never public hosts. */
|
|
27
35
|
export const oysterBuildGitHost = "git.team.internal";
|
|
28
36
|
export const oysterBuildReportHost = "build.team.internal";
|
|
29
|
-
/**
|
|
37
|
+
/** Where an App's source is cloned from. */
|
|
38
|
+
export const oysterBuildLocationSchema = Schema.Union([
|
|
39
|
+
Schema.Struct({
|
|
40
|
+
kind: Schema.Literal("installation"),
|
|
41
|
+
/** The installation whose Git holds the repository (Bonsai for a fork). */
|
|
42
|
+
installationId: Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9_-]{1,256}$/u)),
|
|
43
|
+
repository: Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9._-]{1,128}$/u)),
|
|
44
|
+
}),
|
|
45
|
+
/** The App's own published template (`templateRepositoryForApp`). */
|
|
46
|
+
Schema.Struct({ kind: Schema.Literal("template") }),
|
|
47
|
+
]);
|
|
48
|
+
/** An App's registered build source: where it is cloned and its pinned recipe. */
|
|
30
49
|
export const oysterBuildSourceSchema = Schema.Struct({
|
|
31
50
|
appId: appIdSchema,
|
|
32
|
-
|
|
33
|
-
installationId: Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9_-]{1,256}$/u)),
|
|
34
|
-
repository: Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9._-]{1,128}$/u)),
|
|
51
|
+
location: oysterBuildLocationSchema,
|
|
35
52
|
recipe: capturedPinnedRecipeSchema,
|
|
36
53
|
buildRecipeDigest: sha256DigestSchema,
|
|
37
54
|
});
|
|
55
|
+
/** One Computer as the build owner reaches it. */
|
|
56
|
+
/** The Computer established no terminal result for a build command, for
|
|
57
|
+
* example because its connection to the container dropped mid-command. */
|
|
58
|
+
class BuildOutcomeUnknown extends Error {
|
|
59
|
+
constructor() {
|
|
60
|
+
super("Build outcome unknown");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** The repository a job clones: its installation repository, or its App's template. */
|
|
64
|
+
const cloneRepository = (job) => job.location.kind === "installation"
|
|
65
|
+
? job.location.repository
|
|
66
|
+
: templateRepositoryForApp(job.app_id);
|
|
38
67
|
const jobRow = Schema.Struct({
|
|
39
68
|
job_id: Schema.String,
|
|
40
69
|
app_id: Schema.String,
|
|
41
70
|
release_id: Schema.String,
|
|
42
71
|
source_revision: Schema.String,
|
|
43
|
-
|
|
44
|
-
|
|
72
|
+
/** The job's clone location, as its source named it when the job started. */
|
|
73
|
+
location: Schema.fromJsonString(oysterBuildLocationSchema),
|
|
45
74
|
recipe: Schema.String,
|
|
46
75
|
recipe_digest: Schema.String,
|
|
47
76
|
requested_by: Schema.String,
|
|
@@ -61,6 +90,7 @@ const releaseRow = Schema.Struct({ release: Schema.String });
|
|
|
61
90
|
const sourceCodec = Schema.fromJsonString(oysterBuildSourceSchema);
|
|
62
91
|
const releaseCodec = Schema.fromJsonString(releaseSchema);
|
|
63
92
|
const recipeCodec = Schema.fromJsonString(capturedPinnedRecipeSchema);
|
|
93
|
+
const locationCodec = Schema.fromJsonString(oysterBuildLocationSchema);
|
|
64
94
|
/** Unguessable hex identity from the platform CSPRNG. */
|
|
65
95
|
function randomHex(bytes) {
|
|
66
96
|
return Array.from(crypto.getRandomValues(new Uint8Array(bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
@@ -77,24 +107,75 @@ function sameDigest(left, right) {
|
|
|
77
107
|
difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
|
|
78
108
|
return difference === 0;
|
|
79
109
|
}
|
|
80
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* The SQLite tables one Oyster build owner keeps. Creation and any carrying
|
|
112
|
+
* forward of retained facts run as one transaction of the owner's storage, so
|
|
113
|
+
* an interrupted conversion leaves the facts as they were for a retry.
|
|
114
|
+
*/
|
|
81
115
|
export function migrateOysterAppBuilds(owner) {
|
|
82
|
-
|
|
83
|
-
owner.
|
|
116
|
+
const { sql } = owner;
|
|
117
|
+
owner.transactionSync(() => {
|
|
118
|
+
sql.exec("CREATE TABLE IF NOT EXISTS oyster_build_sources (app_id TEXT PRIMARY KEY, source TEXT NOT NULL) STRICT");
|
|
119
|
+
// Sources recorded before the location union named only an installation
|
|
120
|
+
// repository; carry each forward once, in place.
|
|
121
|
+
sql.exec(`UPDATE oyster_build_sources SET source = json_set(
|
|
122
|
+
json_remove(source, '$.installationId', '$.repository'), '$.location',
|
|
123
|
+
json_object('kind', 'installation',
|
|
124
|
+
'installationId', json_extract(source, '$.installationId'),
|
|
125
|
+
'repository', json_extract(source, '$.repository')))
|
|
126
|
+
WHERE json_type(source, '$.location') IS NULL`);
|
|
127
|
+
const jobColumns = `
|
|
84
128
|
job_id TEXT PRIMARY KEY, app_id TEXT NOT NULL, release_id TEXT NOT NULL UNIQUE,
|
|
85
|
-
source_revision TEXT NOT NULL,
|
|
129
|
+
source_revision TEXT NOT NULL, location TEXT NOT NULL,
|
|
86
130
|
recipe TEXT NOT NULL, recipe_digest TEXT NOT NULL, requested_by TEXT NOT NULL,
|
|
87
131
|
credential_id TEXT NOT NULL, upload_grant_digest TEXT NOT NULL,
|
|
88
132
|
status TEXT NOT NULL, artifact_digest TEXT, artifact_bytes INTEGER, detail TEXT,
|
|
89
|
-
issued_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, executable_until INTEGER NOT NULL
|
|
90
|
-
|
|
91
|
-
|
|
133
|
+
issued_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, executable_until INTEGER NOT NULL`;
|
|
134
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS oyster_build_jobs (${jobColumns}) STRICT`);
|
|
135
|
+
// Jobs recorded before the location union named an installation repository
|
|
136
|
+
// in two columns; carry each into the one location column, once.
|
|
137
|
+
if (sql
|
|
138
|
+
.exec("SELECT name FROM pragma_table_info('oyster_build_jobs') WHERE name='installation_id'")
|
|
139
|
+
.toArray().length > 0) {
|
|
140
|
+
sql.exec(`CREATE TABLE oyster_build_jobs_current (${jobColumns}) STRICT`);
|
|
141
|
+
sql.exec(`INSERT INTO oyster_build_jobs_current SELECT job_id, app_id, release_id,
|
|
142
|
+
source_revision,
|
|
143
|
+
json_object('kind', 'installation', 'installationId', installation_id,
|
|
144
|
+
'repository', repository),
|
|
145
|
+
recipe, recipe_digest, requested_by, credential_id, upload_grant_digest,
|
|
146
|
+
status, artifact_digest, artifact_bytes, detail, issued_at, expires_at,
|
|
147
|
+
executable_until
|
|
148
|
+
FROM oyster_build_jobs`);
|
|
149
|
+
sql.exec("DROP TABLE oyster_build_jobs");
|
|
150
|
+
sql.exec("ALTER TABLE oyster_build_jobs_current RENAME TO oyster_build_jobs");
|
|
151
|
+
}
|
|
152
|
+
sql.exec("CREATE TABLE IF NOT EXISTS oyster_build_releases (release_id TEXT PRIMARY KEY, job_id TEXT NOT NULL UNIQUE, release TEXT NOT NULL) STRICT");
|
|
153
|
+
});
|
|
92
154
|
}
|
|
93
|
-
/** The build owner over one tenant's SQLite, Computer and
|
|
155
|
+
/** The build owner over one tenant's SQLite, Computer and artifact store. */
|
|
94
156
|
export function createOysterAppBuilds(options) {
|
|
95
157
|
const { owner } = options;
|
|
96
158
|
const now = options.now ?? Date.now;
|
|
97
|
-
|
|
159
|
+
// The host store overwrites on put, so write-once is checked here first.
|
|
160
|
+
const storeObjects = {
|
|
161
|
+
async create(artifactRef, bytes) {
|
|
162
|
+
if ((await options.artifacts.get(artifactRef)) !== null)
|
|
163
|
+
return "exists";
|
|
164
|
+
await options.artifacts.put(artifactRef, bytes);
|
|
165
|
+
return "created";
|
|
166
|
+
},
|
|
167
|
+
read: (artifactRef) => options.artifacts.get(artifactRef),
|
|
168
|
+
};
|
|
169
|
+
migrateOysterAppBuilds(owner);
|
|
170
|
+
// A job outlives the owner instance that started it: the Computer keeps
|
|
171
|
+
// building and its own reports (objects, candidate, status) settle the
|
|
172
|
+
// job, so an owner restart must not fail it. A job that reaches its expiry
|
|
173
|
+
// without a result can never settle, so it fails then instead of reading
|
|
174
|
+
// as running for as long as anyone asks.
|
|
175
|
+
const expire = () => owner.sql.exec("UPDATE oyster_build_jobs SET status='failed', detail=? WHERE status='running' AND expires_at<=?", "Build expired without a result", now());
|
|
176
|
+
expire();
|
|
177
|
+
const decodeRecipe = (recipe) => Schema.decodeUnknownSync(recipeCodec)(recipe);
|
|
178
|
+
const grantAdmits = async (job, grant) => sameDigest(job.upload_grant_digest, await sha256Hex(new TextEncoder().encode(grant)));
|
|
98
179
|
const readJob = (jobId) => {
|
|
99
180
|
const [row] = owner.sql
|
|
100
181
|
.exec("SELECT * FROM oyster_build_jobs WHERE job_id=?", jobId)
|
|
@@ -115,9 +196,11 @@ export function createOysterAppBuilds(options) {
|
|
|
115
196
|
computerId: options.computer.name,
|
|
116
197
|
credentialId: job.credential_id,
|
|
117
198
|
issuedAt: job.issued_at,
|
|
118
|
-
repository: job
|
|
199
|
+
repository: cloneRepository(job),
|
|
119
200
|
expiresAt: job.expires_at,
|
|
120
|
-
remote:
|
|
201
|
+
remote: job.location.kind === "installation"
|
|
202
|
+
? `https://${oysterBuildGitHost}/git/${job.location.installationId}/${job.location.repository}.git`
|
|
203
|
+
: `https://${oysterBuildGitHost}/${cloneRepository(job)}.git`,
|
|
121
204
|
reportUrl: `https://${oysterBuildReportHost}/jobs/${job.job_id}`,
|
|
122
205
|
status: job.status,
|
|
123
206
|
artifact: job.artifact_digest === null || job.artifact_bytes === null
|
|
@@ -165,11 +248,16 @@ export function createOysterAppBuilds(options) {
|
|
|
165
248
|
owner.sql.exec("UPDATE oyster_build_jobs SET executable_until=? WHERE job_id=?", decision.status === "allowed" ? decision.expiresAt : 0, decision.jobId);
|
|
166
249
|
return Promise.resolve();
|
|
167
250
|
},
|
|
168
|
-
execute: (input) =>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
251
|
+
execute: async (input) => {
|
|
252
|
+
const result = await options.computer.port.runBuild({
|
|
253
|
+
jobId: input.jobId,
|
|
254
|
+
command: input.command,
|
|
255
|
+
timeoutMs: input.timeoutMs,
|
|
256
|
+
});
|
|
257
|
+
if (result.output === undefined || result.output === null)
|
|
258
|
+
throw new BuildOutcomeUnknown();
|
|
259
|
+
return result;
|
|
260
|
+
},
|
|
173
261
|
deliverTeamGitEvents: () => Promise.resolve(0),
|
|
174
262
|
}),
|
|
175
263
|
});
|
|
@@ -190,7 +278,10 @@ export function createOysterAppBuilds(options) {
|
|
|
190
278
|
});
|
|
191
279
|
}
|
|
192
280
|
catch (cause) {
|
|
193
|
-
|
|
281
|
+
// Without a terminal result the build may still be running on the
|
|
282
|
+
// Computer; its own reports settle the job, or its expiry fails it.
|
|
283
|
+
if (!(cause instanceof BuildOutcomeUnknown))
|
|
284
|
+
settle(jobId, "failed", cause instanceof Error ? cause.message : "Build failed");
|
|
194
285
|
}
|
|
195
286
|
return view(required(jobId));
|
|
196
287
|
};
|
|
@@ -214,6 +305,8 @@ export function createOysterAppBuilds(options) {
|
|
|
214
305
|
readSource,
|
|
215
306
|
/**
|
|
216
307
|
* Mint one build job for an App's registered source at one exact commit.
|
|
308
|
+
* A template source builds only a seed revision recorded for its pinned
|
|
309
|
+
* recipe.
|
|
217
310
|
* Answers the job and its one run, which the caller keeps alive (the
|
|
218
311
|
* upload grant exists only inside that run's fixed command).
|
|
219
312
|
*/
|
|
@@ -222,19 +315,25 @@ export function createOysterAppBuilds(options) {
|
|
|
222
315
|
const source = readSource(input.appId);
|
|
223
316
|
if (source === null)
|
|
224
317
|
throw new Error("App has no build source");
|
|
318
|
+
if (source.location.kind === "template") {
|
|
319
|
+
const receipt = await options.templates.read(source.appId, sourceRevision);
|
|
320
|
+
if (receipt?.buildRecipeDigest !== source.buildRecipeDigest)
|
|
321
|
+
throw new Error("Revision is not a seed of this App's template");
|
|
322
|
+
}
|
|
225
323
|
const jobId = `build_${randomHex(16)}`;
|
|
226
324
|
const releaseId = parseReleaseId(`rel_${sourceRevision.slice("git:".length, "git:".length + 12)}${randomHex(4)}`);
|
|
227
325
|
const uploadGrant = randomHex(32);
|
|
228
326
|
const issuedAt = now();
|
|
229
|
-
owner.sql.exec(`INSERT INTO oyster_build_jobs(job_id, app_id, release_id, source_revision,
|
|
230
|
-
|
|
327
|
+
owner.sql.exec(`INSERT INTO oyster_build_jobs(job_id, app_id, release_id, source_revision, location,
|
|
328
|
+
recipe, recipe_digest, requested_by, credential_id, upload_grant_digest,
|
|
231
329
|
status, artifact_digest, artifact_bytes, detail, issued_at, expires_at, executable_until)
|
|
232
|
-
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
330
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', NULL, NULL, NULL, ?, ?, 0)`, jobId, source.appId, releaseId, sourceRevision, Schema.encodeUnknownSync(locationCodec)(source.location), Schema.encodeUnknownSync(recipeCodec)(source.recipe), source.buildRecipeDigest, input.requestedBy, randomHex(32), await sha256Hex(new TextEncoder().encode(uploadGrant)), issuedAt, issuedAt + jobTtlMs);
|
|
233
331
|
// The grant leaves this owner only inside the fixed command.
|
|
234
332
|
const command = teamAppBuildCommand({ appId: source.appId, sourceRevision, source }, uploadGrant);
|
|
235
333
|
return { build: view(required(jobId)), run: () => run(jobId, command) };
|
|
236
334
|
},
|
|
237
335
|
read(jobId) {
|
|
336
|
+
expire();
|
|
238
337
|
const job = readJob(jobId);
|
|
239
338
|
return job === null ? null : view(job);
|
|
240
339
|
},
|
|
@@ -270,7 +369,9 @@ export function createOysterAppBuilds(options) {
|
|
|
270
369
|
target.scope !== "read")
|
|
271
370
|
return null;
|
|
272
371
|
const job = owner.sql
|
|
273
|
-
.exec(
|
|
372
|
+
.exec(`SELECT * FROM oyster_build_jobs WHERE json_extract(location, '$.kind')='installation'
|
|
373
|
+
AND json_extract(location, '$.installationId')=? AND json_extract(location, '$.repository')=?
|
|
374
|
+
AND status='running' AND expires_at>? ORDER BY issued_at DESC LIMIT 1`, target.installationId, target.repository, now())
|
|
274
375
|
.toArray()
|
|
275
376
|
.map((row) => decodeJob(row))[0];
|
|
276
377
|
return job === undefined
|
|
@@ -284,12 +385,29 @@ export function createOysterAppBuilds(options) {
|
|
|
284
385
|
return null;
|
|
285
386
|
const job = readJob(match[1] ?? "");
|
|
286
387
|
return live(job) &&
|
|
388
|
+
job.location.kind === "installation" &&
|
|
287
389
|
sameDigest(job.credential_id, match[2] ?? "") &&
|
|
288
|
-
job.
|
|
289
|
-
job.repository === target.repository
|
|
390
|
+
job.location.installationId === target.installationId &&
|
|
391
|
+
job.location.repository === target.repository
|
|
290
392
|
? { requestedBy: job.requested_by }
|
|
291
393
|
: null;
|
|
292
394
|
},
|
|
395
|
+
/**
|
|
396
|
+
* May this Computer's clone read this template repository now? Only while
|
|
397
|
+
* a live template job on this Computer names it.
|
|
398
|
+
*/
|
|
399
|
+
admitTemplateClone(computerObjectId, repository) {
|
|
400
|
+
if (computerObjectId !== options.computer.objectId)
|
|
401
|
+
return false;
|
|
402
|
+
// The template repository names exactly one App (templateRepositoryForApp).
|
|
403
|
+
const appId = appForTemplateRepository(repository);
|
|
404
|
+
if (appId === null)
|
|
405
|
+
return false;
|
|
406
|
+
return (owner.sql
|
|
407
|
+
.exec(`SELECT 1 FROM oyster_build_jobs WHERE app_id=? AND json_extract(location, '$.kind')='template'
|
|
408
|
+
AND status='running' AND expires_at>? LIMIT 1`, appId, now())
|
|
409
|
+
.toArray().length > 0);
|
|
410
|
+
},
|
|
293
411
|
/** A reported artifact or status for a live job. */
|
|
294
412
|
record(event) {
|
|
295
413
|
const job = readJob(event.jobId);
|
|
@@ -302,42 +420,68 @@ export function createOysterAppBuilds(options) {
|
|
|
302
420
|
settle(event.jobId, event.status, event.detail);
|
|
303
421
|
},
|
|
304
422
|
/**
|
|
305
|
-
*
|
|
306
|
-
*
|
|
423
|
+
* May this live job's build upload one object now, and how large? The
|
|
424
|
+
* grant is the one inside the job's fixed command; the bound is its
|
|
425
|
+
* recipe's (`pinnedRecipeObjectBound`).
|
|
307
426
|
*/
|
|
308
|
-
async
|
|
427
|
+
async admitObjectUpload(jobId, computerObjectId, grant) {
|
|
428
|
+
const job = readJob(jobId);
|
|
429
|
+
if (computerObjectId !== options.computer.objectId ||
|
|
430
|
+
!live(job) ||
|
|
431
|
+
!(await grantAdmits(job, grant)))
|
|
432
|
+
return null;
|
|
433
|
+
return {
|
|
434
|
+
maxObjectBytes: pinnedRecipeObjectBound(decodeRecipe(job.recipe)),
|
|
435
|
+
};
|
|
436
|
+
},
|
|
437
|
+
/**
|
|
438
|
+
* The Computer's candidate manifest for a live job: checked against the
|
|
439
|
+
* job and its pinned recipe, each named object found stored with its
|
|
440
|
+
* size and verified digest (one at a time; only the backend descriptor
|
|
441
|
+
* is read), its asset manifest and App root written, verified as an
|
|
442
|
+
* installable Release and recorded. An over-policy candidate stores
|
|
443
|
+
* nothing.
|
|
444
|
+
*/
|
|
445
|
+
async submitCandidate(jobId, manifest, grant, credentialId) {
|
|
309
446
|
const job = readJob(jobId);
|
|
310
447
|
if (!live(job) ||
|
|
311
448
|
job.credential_id !== credentialId ||
|
|
312
|
-
!
|
|
449
|
+
!(await grantAdmits(job, grant)))
|
|
313
450
|
return new Response(null, { status: 403 });
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
|
|
451
|
+
if (manifest.appId !== job.app_id ||
|
|
452
|
+
manifest.provenance.sourceRevision !== job.source_revision ||
|
|
453
|
+
manifest.provenance.buildRecipeDigest !== job.recipe_digest)
|
|
317
454
|
return new Response(null, { status: 409 });
|
|
318
455
|
if (readRelease(jobId) !== null)
|
|
319
456
|
return new Response(null, { status: 409 });
|
|
320
|
-
const recipe =
|
|
321
|
-
const digest = await sha256Hex(new TextEncoder().encode(encodeTeamAppCandidateSubmission(candidate)));
|
|
322
|
-
const writerOptions = {
|
|
323
|
-
bucket: options.bucket,
|
|
324
|
-
artifactRef: parseArtifactRef(`artifact:sha256-${digest}`),
|
|
325
|
-
policy: {
|
|
326
|
-
maxBytes: recipe.maxOutputBytes,
|
|
327
|
-
maxManifestBytes: recipe.assets?.maxManifestBytes ?? 1024 * 1024,
|
|
328
|
-
},
|
|
329
|
-
};
|
|
330
|
-
if (recipe.assets !== undefined)
|
|
331
|
-
writerOptions.assets = recipe.assets;
|
|
457
|
+
const recipe = decodeRecipe(job.recipe);
|
|
332
458
|
let release;
|
|
333
459
|
try {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
460
|
+
if (!fitsPinnedRecipe(manifest.output, recipe))
|
|
461
|
+
throw new Error("Candidate exceeds its pinned recipe");
|
|
462
|
+
for (const object of candidateManifestObjects(manifest.output)) {
|
|
463
|
+
const head = await options.artifacts.head(contentObjectRef(object.digest));
|
|
464
|
+
if (head?.byteLength !== object.byteLength ||
|
|
465
|
+
head.digest !== object.digest)
|
|
466
|
+
throw new Error("Candidate object is not stored as named");
|
|
467
|
+
}
|
|
468
|
+
if (manifest.output.kind !== "ui")
|
|
469
|
+
decodeCandidateBackend(await readVerified(options.artifacts, contentObjectRef(manifest.output.backend.digest), manifest.output.backend.digest));
|
|
470
|
+
const root = await writeArtifactRoot(manifest.output, storeObjects, {
|
|
471
|
+
rootArtifactRef: parseArtifactRef(`artifact:sha256-${await digestTeamAppCandidateManifest(manifest)}`),
|
|
472
|
+
assetPolicy: recipe.assets,
|
|
473
|
+
});
|
|
474
|
+
release = Schema.decodeUnknownSync(releaseSchema)({
|
|
475
|
+
releaseId: job.release_id,
|
|
476
|
+
appId: manifest.appId,
|
|
477
|
+
artifact: {
|
|
478
|
+
artifactRef: root.artifactRef,
|
|
479
|
+
artifactDigest: root.artifactDigest,
|
|
480
|
+
provenance: manifest.provenance,
|
|
338
481
|
},
|
|
339
|
-
})
|
|
340
|
-
|
|
482
|
+
});
|
|
483
|
+
// Each object was checked by its stored digest above.
|
|
484
|
+
await verifyRelease(options.artifacts, release, defaultAssetPolicy, false);
|
|
341
485
|
}
|
|
342
486
|
catch {
|
|
343
487
|
settle(jobId, "failed", "Candidate could not be stored as a Release");
|
package/dist/app-sources.d.ts
CHANGED
|
@@ -22,25 +22,10 @@ export declare function createOysterArtifactsRepositoryMetadataStore(sql: Watchd
|
|
|
22
22
|
* writes (`services/app-publisher/src/source-git.ts`), scoped to Oyster's own
|
|
23
23
|
* Artifacts composition since a package cannot depend on that service. */
|
|
24
24
|
export declare function exchangeOysterTemplateGit(artifacts: Pick<Artifacts, "exchangeGit">, remote: string, repository: string, request: Request, permission?: "read" | "write"): Promise<Response>;
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
readonly receipt?: undefined;
|
|
33
|
-
} | {
|
|
34
|
-
readonly kind: "recorded" | "existing";
|
|
35
|
-
readonly receipt: {
|
|
36
|
-
readonly appId: import("@fungi.computer/puffball").AppId;
|
|
37
|
-
readonly repository: string;
|
|
38
|
-
readonly sourceRevision: import("@fungi.computer/puffball").SourceRevision;
|
|
39
|
-
readonly sourceTreeDigest: import("@fungi.computer/puffball").Sha256Digest;
|
|
40
|
-
readonly seedRevision: import("@fungi.computer/puffball").SourceRevision;
|
|
41
|
-
readonly buildRecipeDigest: import("@fungi.computer/puffball").Sha256Digest;
|
|
42
|
-
};
|
|
43
|
-
} | {
|
|
44
|
-
readonly kind: "unavailable";
|
|
45
|
-
readonly receipt?: undefined;
|
|
46
|
-
}>;
|
|
25
|
+
/** The operator's smart-HTTP door to one App's template repository: an
|
|
26
|
+
* ordinary `git push` (or the fetch before it) to
|
|
27
|
+
* `<base>/<suffix>` reaches `template-<app>` through the same admitted
|
|
28
|
+
* Artifacts exchange, which streams both bodies and holds every provider
|
|
29
|
+
* credential. An advertisement first opens the repository, so a fresh Oyster's
|
|
30
|
+
* first seed creates it. Admission (the owner) is the caller's. */
|
|
31
|
+
export declare function exchangeOysterTemplateSource(artifacts: Pick<Artifacts, "ensureRepository" | "exchangeGit">, rawAppId: string, suffix: string, request: Request): Promise<Response>;
|
package/dist/app-sources.js
CHANGED
|
@@ -4,7 +4,8 @@ import { appIdSchema, sha256DigestSchema, sourceRevisionSchema, } from "@fungi.c
|
|
|
4
4
|
import { teamAppPublisherRegistrationSchema, } from "@fungi.computer/puffball/publisher";
|
|
5
5
|
import { capturedPinnedRecipeSchema, digestPinnedRecipe, } from "./build-contract.js";
|
|
6
6
|
import { sameReceipt, seedReceiptInputSchema, templateRepositoryForApp, } from "./source/index.js";
|
|
7
|
-
import {
|
|
7
|
+
import { smartHttpExchange } from "./git-door.js";
|
|
8
|
+
import { parseAppId } from "@fungi.computer/puffball";
|
|
8
9
|
/**
|
|
9
10
|
* Oyster's own storage for App source templates, Team-owned fork
|
|
10
11
|
* registrations and the recorded Artifacts repository metadata their fork
|
|
@@ -207,20 +208,21 @@ export function createOysterArtifactsRepositoryMetadataStore(sql) {
|
|
|
207
208
|
initialToken: decoded.initial_token,
|
|
208
209
|
}));
|
|
209
210
|
};
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return null;
|
|
211
|
+
/** The active row alone; activation checks its own promotion with this. */
|
|
212
|
+
const readActive = (name) => {
|
|
213
213
|
const row = sql
|
|
214
214
|
.exec(`SELECT repository_name, provider_id, default_branch, remote,
|
|
215
215
|
parent_repository
|
|
216
216
|
FROM oyster_artifacts_repositories WHERE repository_name = ?`, name)
|
|
217
217
|
.toArray()[0];
|
|
218
|
-
return row === undefined
|
|
218
|
+
return Promise.resolve(row === undefined
|
|
219
219
|
? null
|
|
220
220
|
: metadataFromRow(Schema.decodeUnknownSync(repositoryRowSchema, {
|
|
221
221
|
onExcessProperty: "error",
|
|
222
|
-
})(row));
|
|
222
|
+
})(row)));
|
|
223
223
|
};
|
|
224
|
+
/** Public reads hide a repository while its initial token is pending. */
|
|
225
|
+
const read = async (name) => (await readPending(name)) === null ? readActive(name) : null;
|
|
224
226
|
return Object.freeze({
|
|
225
227
|
read,
|
|
226
228
|
readPending,
|
|
@@ -253,7 +255,7 @@ export function createOysterArtifactsRepositoryMetadataStore(sql) {
|
|
|
253
255
|
FROM oyster_artifacts_repositories_pending
|
|
254
256
|
WHERE repository_name = ? AND provider_id = ? AND default_branch = ?
|
|
255
257
|
AND remote = ? AND parent_repository IS ? AND initial_token = ?`, metadata.name, metadata.id, metadata.defaultBranch, metadata.remote, metadata.parentRepository, initialToken);
|
|
256
|
-
const active = await
|
|
258
|
+
const active = await readActive(metadata.name);
|
|
257
259
|
if (active === null || !sameMetadata(active, metadata))
|
|
258
260
|
throw new Error("Oyster Artifacts metadata conflicts");
|
|
259
261
|
sql.exec(`DELETE FROM oyster_artifacts_repositories_pending
|
|
@@ -324,12 +326,45 @@ export async function exchangeOysterTemplateGit(artifacts, remote, repository, r
|
|
|
324
326
|
headers: { "Content-Type": type, "Cache-Control": "no-store" },
|
|
325
327
|
});
|
|
326
328
|
}
|
|
327
|
-
/**
|
|
328
|
-
*
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
329
|
+
/** The operator's smart-HTTP door to one App's template repository: an
|
|
330
|
+
* ordinary `git push` (or the fetch before it) to
|
|
331
|
+
* `<base>/<suffix>` reaches `template-<app>` through the same admitted
|
|
332
|
+
* Artifacts exchange, which streams both bodies and holds every provider
|
|
333
|
+
* credential. An advertisement first opens the repository, so a fresh Oyster's
|
|
334
|
+
* first seed creates it. Admission (the owner) is the caller's. */
|
|
335
|
+
export async function exchangeOysterTemplateSource(artifacts, rawAppId, suffix, request) {
|
|
336
|
+
let appId;
|
|
337
|
+
try {
|
|
338
|
+
appId = parseAppId(rawAppId);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return new Response("Not an App", { status: 404 });
|
|
342
|
+
}
|
|
343
|
+
const url = new URL(request.url);
|
|
344
|
+
const exchange = smartHttpExchange(request.method, url, suffix);
|
|
345
|
+
if (exchange === null)
|
|
346
|
+
return new Response("Not a git route", { status: 404 });
|
|
347
|
+
// The provider exchange carries no content coding; refuse rather than corrupt.
|
|
348
|
+
if (request.headers.has("content-encoding"))
|
|
349
|
+
return new Response("Compressed Git requests are not supported", {
|
|
350
|
+
status: 400,
|
|
351
|
+
});
|
|
352
|
+
const repository = templateRepositoryForApp(appId);
|
|
353
|
+
if (exchange.advertise)
|
|
354
|
+
await artifacts.ensureRepository(repository);
|
|
355
|
+
const headers = new Headers();
|
|
356
|
+
for (const name of ["content-type", "accept", "git-protocol"]) {
|
|
357
|
+
const value = request.headers.get(name);
|
|
358
|
+
if (value !== null)
|
|
359
|
+
headers.set(name, value);
|
|
360
|
+
}
|
|
361
|
+
const init = {
|
|
362
|
+
method: request.method,
|
|
363
|
+
headers,
|
|
364
|
+
body: exchange.advertise ? null : request.body,
|
|
365
|
+
signal: request.signal,
|
|
333
366
|
};
|
|
334
|
-
|
|
367
|
+
if (init.body !== null)
|
|
368
|
+
Object.assign(init, { duplex: "half" });
|
|
369
|
+
return exchangeOysterTemplateGit(artifacts, `https://git.team.internal/${repository}.git`, repository, new Request(`https://git.team.internal/${repository}.git/${suffix}${url.search}`, init), "write");
|
|
335
370
|
}
|
package/dist/apps/gate.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type HostPrincipal = Readonly<{
|
|
|
10
10
|
role: "owner" | "member" | null;
|
|
11
11
|
}>;
|
|
12
12
|
/** Requests select existing SDK methods, opaque backend calls and derived operation identities. */
|
|
13
|
-
export declare const gateRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
13
|
+
export declare const gateRequestSchema: z.ZodLazy<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
14
14
|
kind: z.ZodLiteral<"method">;
|
|
15
15
|
name: z.ZodString;
|
|
16
16
|
input: z.ZodUnknown;
|
|
@@ -22,7 +22,7 @@ export declare const gateRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
22
22
|
}>;
|
|
23
23
|
}, z.core.$strict>, z.ZodObject<{
|
|
24
24
|
kind: z.ZodLiteral<"capability-route">;
|
|
25
|
-
capability: z.ZodPipe<z.ZodUnknown, z.ZodTransform<"agent.list" | "agent.manage" | "agent.rename" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "llm.generation" | "media.generation" | "games:servers" | "git.repository" | "apps.open" | "apps.call" | "app.storage" | "app.events.emit" | "team.workbenchHost" | "marketplace.browse" | "marketplace.purchase" | "workflow.run" | "workflow.publish" | "workflow.arm" | "world" | "channels.read" | "channels.personal.manage", unknown>>;
|
|
25
|
+
capability: z.ZodPipe<z.ZodUnknown, z.ZodTransform<"agent.list" | "agent.manage" | "agent.rename" | "agent.avatar" | "agent.toolPolicy" | "agent.read" | "agent.send" | "computer.list" | "computer.readFile" | "computer.writeFile" | "computer.execute" | "llm.generation" | "media.generation" | "games:servers" | "git.repository" | "apps.open" | "apps.call" | "app.storage" | "app.events.emit" | "team.workbenchHost" | "marketplace.browse" | "marketplace.purchase" | "workflow.run" | "workflow.publish" | "workflow.arm" | "world" | "channels.read" | "channels.personal.manage", unknown>>;
|
|
26
26
|
operation: z.ZodString;
|
|
27
27
|
}, z.core.$strict>, z.ZodObject<{
|
|
28
28
|
kind: z.ZodLiteral<"binding">;
|
|
@@ -32,7 +32,7 @@ export declare const gateRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
32
32
|
kind: z.ZodLiteral<"list-tools">;
|
|
33
33
|
}, z.core.$strict>, z.ZodObject<{
|
|
34
34
|
kind: z.ZodLiteral<"backend">;
|
|
35
|
-
}, z.core.$strict>], "kind"
|
|
35
|
+
}, z.core.$strict>], "kind">>;
|
|
36
36
|
/** One decoded method, route, binding or catalog admission request. */
|
|
37
37
|
export type GateRequest = z.output<typeof gateRequestSchema>;
|
|
38
38
|
/** Immutable values the host already read; resource/grantor freshness remains host-owned. */
|
|
@@ -49,16 +49,16 @@ export type GateFacts = Readonly<{
|
|
|
49
49
|
}>;
|
|
50
50
|
}>;
|
|
51
51
|
/** Binding grants have the exact S2 InvocationFacts shape and its maintained registry grammar. */
|
|
52
|
-
export declare const invocationGrantsSchema: z.ZodReadonly<z.ZodArray<z.ZodObject<{
|
|
52
|
+
export declare const invocationGrantsSchema: z.ZodLazy<z.ZodReadonly<z.ZodArray<z.ZodObject<{
|
|
53
53
|
binding: z.ZodPipe<z.ZodUnknown, z.ZodTransform<"BACKGROUND" | "GIT" | "EVENTS" | "APPS" | "WORKFLOWS" | "APP_STORAGE" | "MEDIA" | "COMPUTER" | "INBOX" | "AGENTS", unknown>>;
|
|
54
54
|
operations: z.ZodReadonly<z.ZodArray<z.ZodString>>;
|
|
55
|
-
}, z.core.$strict
|
|
55
|
+
}, z.core.$strict>>>>;
|
|
56
56
|
/** The explicit binding ceiling passed to an invocation frame. */
|
|
57
57
|
export type InvocationGrants = z.output<typeof invocationGrantsSchema>;
|
|
58
58
|
/** A declared method from the canonical Puffball API grammar. */
|
|
59
59
|
export type AppMethodDeclaration = AppApi["methods"][string];
|
|
60
60
|
/** Ordered, closed refusal vocabulary; external methods always require approval. */
|
|
61
|
-
export declare const gateRefusalSchema: z.ZodObject<{
|
|
61
|
+
export declare const gateRefusalSchema: z.ZodLazy<z.ZodObject<{
|
|
62
62
|
kind: z.ZodLiteral<"refused">;
|
|
63
63
|
code: z.ZodEnum<{
|
|
64
64
|
unavailable: "unavailable";
|
|
@@ -71,7 +71,7 @@ export declare const gateRefusalSchema: z.ZodObject<{
|
|
|
71
71
|
invalid_input: "invalid_input";
|
|
72
72
|
visitor_read_only: "visitor_read_only";
|
|
73
73
|
}>;
|
|
74
|
-
}, z.core.$strict
|
|
74
|
+
}, z.core.$strict>>;
|
|
75
75
|
/** Either an admitted scoped ceiling or one closed refusal code. */
|
|
76
76
|
export type GateDecision = Readonly<{
|
|
77
77
|
kind: "admitted";
|
package/dist/apps/gate.js
CHANGED
|
@@ -8,7 +8,7 @@ import { appToolAccessSchema } from "./tools.js";
|
|
|
8
8
|
import { ownerCodec, } from "./installation.js";
|
|
9
9
|
const bindingNameSchema = ownerCodec(Schema.Literals(Object.values(appBackendBindingKinds).map((kind) => kind.name)));
|
|
10
10
|
/** Requests select existing SDK methods, opaque backend calls and derived operation identities. */
|
|
11
|
-
export const gateRequestSchema = z.discriminatedUnion("kind", [
|
|
11
|
+
export const gateRequestSchema = z.lazy(() => z.discriminatedUnion("kind", [
|
|
12
12
|
z.strictObject({
|
|
13
13
|
kind: z.literal("method"),
|
|
14
14
|
name: z.string(),
|
|
@@ -27,9 +27,9 @@ export const gateRequestSchema = z.discriminatedUnion("kind", [
|
|
|
27
27
|
}),
|
|
28
28
|
z.strictObject({ kind: z.literal("list-tools") }),
|
|
29
29
|
z.strictObject({ kind: z.literal("backend") }),
|
|
30
|
-
]);
|
|
30
|
+
]));
|
|
31
31
|
/** Binding grants have the exact S2 InvocationFacts shape and its maintained registry grammar. */
|
|
32
|
-
export const invocationGrantsSchema = z
|
|
32
|
+
export const invocationGrantsSchema = z.lazy(() => z
|
|
33
33
|
.array(z.strictObject({
|
|
34
34
|
binding: bindingNameSchema,
|
|
35
35
|
operations: z.array(z.string().min(1)).readonly(),
|
|
@@ -38,9 +38,9 @@ export const invocationGrantsSchema = z
|
|
|
38
38
|
name: grant.binding,
|
|
39
39
|
operations: grant.operations,
|
|
40
40
|
}))), "Invalid declared binding operations")
|
|
41
|
-
.readonly();
|
|
41
|
+
.readonly());
|
|
42
42
|
/** Ordered, closed refusal vocabulary; external methods always require approval. */
|
|
43
|
-
export const gateRefusalSchema = z.strictObject({
|
|
43
|
+
export const gateRefusalSchema = z.lazy(() => z.strictObject({
|
|
44
44
|
kind: z.literal("refused"),
|
|
45
45
|
code: z.enum([
|
|
46
46
|
"unavailable",
|
|
@@ -53,7 +53,7 @@ export const gateRefusalSchema = z.strictObject({
|
|
|
53
53
|
"invalid_input",
|
|
54
54
|
"visitor_read_only",
|
|
55
55
|
]),
|
|
56
|
-
});
|
|
56
|
+
}));
|
|
57
57
|
/** Intersect stored approvals with the current verified Release without widening resource selectors. */
|
|
58
58
|
export function grantsFor(installation, declaration, snapshot, capability) {
|
|
59
59
|
if (installation.status !== "enabled" ||
|