@vendoai/vendo 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +28 -0
- package/bin/vendo.mjs +4 -0
- package/dist/ai-sdk.d.ts +35 -0
- package/dist/ai-sdk.js +62 -0
- package/dist/auth-presets/auth-js.d.ts +14 -0
- package/dist/auth-presets/auth-js.js +91 -0
- package/dist/auth-presets/auth0.d.ts +23 -0
- package/dist/auth-presets/auth0.js +98 -0
- package/dist/auth-presets/clerk.d.ts +23 -0
- package/dist/auth-presets/clerk.js +65 -0
- package/dist/auth-presets/conformance.d.ts +42 -0
- package/dist/auth-presets/conformance.js +139 -0
- package/dist/auth-presets/identity.d.ts +56 -0
- package/dist/auth-presets/identity.js +152 -0
- package/dist/auth-presets/index.d.ts +14 -0
- package/dist/auth-presets/index.js +12 -0
- package/dist/auth-presets/jwt.d.ts +16 -0
- package/dist/auth-presets/jwt.js +54 -0
- package/dist/auth-presets/shared.d.ts +35 -0
- package/dist/auth-presets/shared.js +1 -0
- package/dist/auth-presets/supabase.d.ts +42 -0
- package/dist/auth-presets/supabase.js +229 -0
- package/dist/byo-approvals.d.ts +37 -0
- package/dist/byo-approvals.js +189 -0
- package/dist/capability-misses.d.ts +38 -0
- package/dist/capability-misses.js +172 -0
- package/dist/catalog.d.ts +19 -0
- package/dist/catalog.js +152 -0
- package/dist/cli/cloud/args.d.ts +3 -0
- package/dist/cli/cloud/args.js +32 -0
- package/dist/cli/cloud/auth.d.ts +17 -0
- package/dist/cli/cloud/auth.js +123 -0
- package/dist/cli/cloud/client.d.ts +28 -0
- package/dist/cli/cloud/client.js +133 -0
- package/dist/cli/cloud/command.d.ts +22 -0
- package/dist/cli/cloud/command.js +73 -0
- package/dist/cli/cloud/deploy.d.ts +26 -0
- package/dist/cli/cloud/deploy.js +187 -0
- package/dist/cli/cloud/device-login.d.ts +32 -0
- package/dist/cli/cloud/device-login.js +259 -0
- package/dist/cli/cloud/index.d.ts +5 -0
- package/dist/cli/cloud/index.js +59 -0
- package/dist/cli/cloud/keys.d.ts +2 -0
- package/dist/cli/cloud/keys.js +31 -0
- package/dist/cli/cloud/members.d.ts +3 -0
- package/dist/cli/cloud/members.js +25 -0
- package/dist/cli/cloud/output.d.ts +4 -0
- package/dist/cli/cloud/output.js +12 -0
- package/dist/cli/cloud/pending-claim.d.ts +29 -0
- package/dist/cli/cloud/pending-claim.js +38 -0
- package/dist/cli/cloud/read.d.ts +3 -0
- package/dist/cli/cloud/read.js +12 -0
- package/dist/cli/cloud/services.d.ts +2 -0
- package/dist/cli/cloud/services.js +9 -0
- package/dist/cli/cloud/session.d.ts +13 -0
- package/dist/cli/cloud/session.js +41 -0
- package/dist/cli/cloud-init.d.ts +60 -0
- package/dist/cli/cloud-init.js +149 -0
- package/dist/cli/dep-versions.d.ts +26 -0
- package/dist/cli/dep-versions.js +77 -0
- package/dist/cli/doctor-codes.d.ts +58 -0
- package/dist/cli/doctor-codes.js +60 -0
- package/dist/cli/doctor-live.d.ts +67 -0
- package/dist/cli/doctor-live.js +169 -0
- package/dist/cli/doctor.d.ts +52 -0
- package/dist/cli/doctor.js +467 -0
- package/dist/cli/eject.d.ts +29 -0
- package/dist/cli/eject.js +141 -0
- package/dist/cli/extract/apply.d.ts +26 -0
- package/dist/cli/extract/apply.js +123 -0
- package/dist/cli/extract/claude-cli-harness.d.ts +18 -0
- package/dist/cli/extract/claude-cli-harness.js +132 -0
- package/dist/cli/extract/claude-harness.d.ts +14 -0
- package/dist/cli/extract/claude-harness.js +127 -0
- package/dist/cli/extract/codex-cli-harness.d.ts +31 -0
- package/dist/cli/extract/codex-cli-harness.js +154 -0
- package/dist/cli/extract/delegate.d.ts +21 -0
- package/dist/cli/extract/delegate.js +77 -0
- package/dist/cli/extract/extraction.d.ts +65 -0
- package/dist/cli/extract/extraction.js +300 -0
- package/dist/cli/extract/gateway-fuel.d.ts +69 -0
- package/dist/cli/extract/gateway-fuel.js +69 -0
- package/dist/cli/extract/harness.d.ts +114 -0
- package/dist/cli/extract/harness.js +84 -0
- package/dist/cli/extract/index.d.ts +9 -0
- package/dist/cli/extract/index.js +9 -0
- package/dist/cli/extract/npx-engine-harness.d.ts +73 -0
- package/dist/cli/extract/npx-engine-harness.js +225 -0
- package/dist/cli/extract/stages.d.ts +149 -0
- package/dist/cli/extract/stages.js +398 -0
- package/dist/cli/framework.d.ts +8 -0
- package/dist/cli/framework.js +36 -0
- package/dist/cli/init-auth.d.ts +67 -0
- package/dist/cli/init-auth.js +142 -0
- package/dist/cli/init-scaffolds.d.ts +28 -0
- package/dist/cli/init-scaffolds.js +235 -0
- package/dist/cli/init.d.ts +115 -0
- package/dist/cli/init.js +1008 -0
- package/dist/cli/mcp/index.d.ts +7 -0
- package/dist/cli/mcp/index.js +59 -0
- package/dist/cli/mcp/registry.d.ts +9 -0
- package/dist/cli/mcp/registry.js +117 -0
- package/dist/cli/mcp/server-json.d.ts +12 -0
- package/dist/cli/mcp/server-json.js +65 -0
- package/dist/cli/mcp/server.schema.json +574 -0
- package/dist/cli/mcp/verify-domain.d.ts +11 -0
- package/dist/cli/mcp/verify-domain.js +48 -0
- package/dist/cli/playground/app/embed-entry.d.ts +20 -0
- package/dist/cli/playground/app/embed-entry.js +46 -0
- package/dist/cli/playground/app/fake-client.d.ts +3 -0
- package/dist/cli/playground/app/fake-client.js +215 -0
- package/dist/cli/playground/app/fixtures.d.ts +47 -0
- package/dist/cli/playground/app/fixtures.js +472 -0
- package/dist/cli/playground/app/main.d.ts +1 -0
- package/dist/cli/playground/app/main.js +108 -0
- package/dist/cli/playground/app/scenario-mount.d.ts +13 -0
- package/dist/cli/playground/app/scenario-mount.js +48 -0
- package/dist/cli/playground/app/scenarios.d.ts +25 -0
- package/dist/cli/playground/app/scenarios.js +152 -0
- package/dist/cli/playground/app/theme-editor.d.ts +13 -0
- package/dist/cli/playground/app/theme-editor.js +149 -0
- package/dist/cli/playground/app/theme-state.d.ts +29 -0
- package/dist/cli/playground/app/theme-state.js +151 -0
- package/dist/cli/playground/bundle.gen.d.ts +2 -0
- package/dist/cli/playground/bundle.gen.js +2 -0
- package/dist/cli/playground/embed-bundle.gen.d.ts +2 -0
- package/dist/cli/playground/embed-bundle.gen.js +2 -0
- package/dist/cli/playground.d.ts +33 -0
- package/dist/cli/playground.js +120 -0
- package/dist/cli/pretty.d.ts +48 -0
- package/dist/cli/pretty.js +330 -0
- package/dist/cli/refine.d.ts +43 -0
- package/dist/cli/refine.js +181 -0
- package/dist/cli/semantics.d.ts +31 -0
- package/dist/cli/semantics.js +122 -0
- package/dist/cli/shared.d.ts +68 -0
- package/dist/cli/shared.js +184 -0
- package/dist/cli/sync.d.ts +36 -0
- package/dist/cli/sync.js +202 -0
- package/dist/cli/theme/color.d.ts +15 -0
- package/dist/cli/theme/color.js +188 -0
- package/dist/cli/theme/css-vars.d.ts +13 -0
- package/dist/cli/theme/css-vars.js +40 -0
- package/dist/cli/theme/entry-candidates.d.ts +5 -0
- package/dist/cli/theme/entry-candidates.js +22 -0
- package/dist/cli/theme/extract-theme.d.ts +184 -0
- package/dist/cli/theme/extract-theme.js +358 -0
- package/dist/cli/theme/walk.d.ts +2 -0
- package/dist/cli/theme/walk.js +24 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +333 -0
- package/dist/cloud-apps.d.ts +17 -0
- package/dist/cloud-apps.js +46 -0
- package/dist/cloud-console.d.ts +32 -0
- package/dist/cloud-console.js +72 -0
- package/dist/cloud-tools.d.ts +23 -0
- package/dist/cloud-tools.js +196 -0
- package/dist/connections.d.ts +71 -0
- package/dist/connections.js +192 -0
- package/dist/deployment-identity.d.ts +12 -0
- package/dist/deployment-identity.js +69 -0
- package/dist/dev-creds/model.d.ts +65 -0
- package/dist/dev-creds/model.js +159 -0
- package/dist/dev-creds/resolve.d.ts +36 -0
- package/dist/dev-creds/resolve.js +57 -0
- package/dist/hosted-store.d.ts +52 -0
- package/dist/hosted-store.js +322 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1 -0
- package/dist/mastra.d.ts +46 -0
- package/dist/mastra.js +88 -0
- package/dist/react.d.ts +10 -0
- package/dist/react.js +32 -0
- package/dist/refine.d.ts +244 -0
- package/dist/refine.js +599 -0
- package/dist/remixable.d.ts +18 -0
- package/dist/remixable.js +42 -0
- package/dist/runtime-capture.d.ts +18 -0
- package/dist/runtime-capture.js +111 -0
- package/dist/sandbox-wire.d.ts +101 -0
- package/dist/sandbox-wire.js +101 -0
- package/dist/sandbox.d.ts +38 -0
- package/dist/sandbox.js +389 -0
- package/dist/server.d.ts +216 -0
- package/dist/server.js +1320 -0
- package/dist/sync-impact.d.ts +14 -0
- package/dist/sync-impact.js +75 -0
- package/dist/turn-liveness.d.ts +33 -0
- package/dist/turn-liveness.js +105 -0
- package/dist/wire/approvals.d.ts +10 -0
- package/dist/wire/approvals.js +59 -0
- package/dist/wire/apps.d.ts +5 -0
- package/dist/wire/apps.js +155 -0
- package/dist/wire/automations.d.ts +5 -0
- package/dist/wire/automations.js +57 -0
- package/dist/wire/box.d.ts +3 -0
- package/dist/wire/box.js +268 -0
- package/dist/wire/connections.d.ts +5 -0
- package/dist/wire/connections.js +47 -0
- package/dist/wire/context.d.ts +22 -0
- package/dist/wire/context.js +190 -0
- package/dist/wire/doctor.d.ts +19 -0
- package/dist/wire/doctor.js +116 -0
- package/dist/wire/misc.d.ts +18 -0
- package/dist/wire/misc.js +227 -0
- package/dist/wire/shared.d.ts +158 -0
- package/dist/wire/shared.js +134 -0
- package/dist/wire/threads.d.ts +3 -0
- package/dist/wire/threads.js +68 -0
- package/package.json +151 -0
- package/skills/vendo-setup/SKILL.md +128 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { VendoStore } from "@vendoai/store";
|
|
2
|
+
export interface ToolImpact {
|
|
3
|
+
tool: string;
|
|
4
|
+
apps: {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
}[];
|
|
8
|
+
automations: {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
}[];
|
|
12
|
+
grants: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function computeImpact(store: VendoStore, tools: string[]): Promise<ToolImpact[]>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { VENDO_TREE_FORMAT_V2 } from "@vendoai/core";
|
|
2
|
+
async function allRecords(store, collection) {
|
|
3
|
+
const records = [];
|
|
4
|
+
let cursor;
|
|
5
|
+
do {
|
|
6
|
+
const page = await store.records(collection).list({ limit: 1_000, cursor });
|
|
7
|
+
records.push(...page.records);
|
|
8
|
+
cursor = page.cursor;
|
|
9
|
+
} while (cursor !== undefined);
|
|
10
|
+
return records;
|
|
11
|
+
}
|
|
12
|
+
function collectActions(value, tools) {
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
for (const item of value)
|
|
15
|
+
collectActions(item, tools);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (typeof value !== "object" || value === null)
|
|
19
|
+
return;
|
|
20
|
+
const record = value;
|
|
21
|
+
for (const key of ["action", "$action"]) {
|
|
22
|
+
const reference = record[key];
|
|
23
|
+
if (typeof reference === "string" && !reference.startsWith("fn:"))
|
|
24
|
+
tools.add(reference);
|
|
25
|
+
}
|
|
26
|
+
for (const nested of Object.values(record))
|
|
27
|
+
collectActions(nested, tools);
|
|
28
|
+
}
|
|
29
|
+
function referencedTools(doc) {
|
|
30
|
+
const tools = new Set();
|
|
31
|
+
if (doc.tree?.formatVersion === VENDO_TREE_FORMAT_V2) {
|
|
32
|
+
const tree = doc.tree;
|
|
33
|
+
for (const query of tree.queries ?? []) {
|
|
34
|
+
if (typeof query.tool === "string" && !query.tool.startsWith("fn:"))
|
|
35
|
+
tools.add(query.tool);
|
|
36
|
+
}
|
|
37
|
+
for (const node of tree.nodes ?? [])
|
|
38
|
+
collectActions(node.props, tools);
|
|
39
|
+
}
|
|
40
|
+
if (doc.trigger?.run.kind === "steps") {
|
|
41
|
+
for (const step of doc.trigger.run.steps) {
|
|
42
|
+
if (!step.tool.startsWith("fn:"))
|
|
43
|
+
tools.add(step.tool);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return tools;
|
|
47
|
+
}
|
|
48
|
+
function activeGrant(grant, now) {
|
|
49
|
+
return grant.revokedAt === undefined && (grant.expiresAt === undefined || grant.expiresAt > now);
|
|
50
|
+
}
|
|
51
|
+
export async function computeImpact(store, tools) {
|
|
52
|
+
const [appRecords, grantRecords] = await Promise.all([
|
|
53
|
+
allRecords(store, "vendo_apps"),
|
|
54
|
+
allRecords(store, "vendo_grants"),
|
|
55
|
+
]);
|
|
56
|
+
const apps = appRecords.map((record) => record.data).filter((app) => app.enabled);
|
|
57
|
+
const now = new Date().toISOString();
|
|
58
|
+
const grants = grantRecords
|
|
59
|
+
.map((record) => record.data)
|
|
60
|
+
.filter((grant) => activeGrant(grant, now));
|
|
61
|
+
return tools.map((tool) => {
|
|
62
|
+
const impact = { tool, apps: [], automations: [], grants: 0 };
|
|
63
|
+
for (const app of apps) {
|
|
64
|
+
if (!referencedTools(app.doc).has(tool))
|
|
65
|
+
continue;
|
|
66
|
+
const reference = { id: app.doc.id, title: app.doc.name };
|
|
67
|
+
if (app.doc.trigger === undefined)
|
|
68
|
+
impact.apps.push(reference);
|
|
69
|
+
else
|
|
70
|
+
impact.automations.push(reference);
|
|
71
|
+
}
|
|
72
|
+
impact.grants = grants.filter((grant) => grant.tool === tool).length;
|
|
73
|
+
return impact;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** ENG-353 — server-side turn liveness: the idle-abort fallback for client
|
|
2
|
+
* disconnects the runtime never surfaces.
|
|
3
|
+
*
|
|
4
|
+
* The fast path is unchanged: `request.signal` cancels the turn the moment the
|
|
5
|
+
* runtime propagates a fetch abort (wave-5 AGENT-3). But under `next dev` a
|
|
6
|
+
* real browser's graceful tab-close/navigate-away fires neither the signal nor
|
|
7
|
+
* a stream cancel, so an abandoned turn runs to completion. The fallback is
|
|
8
|
+
* liveness by heartbeat: the panel beats `POST /threads/:id/heartbeat` while
|
|
9
|
+
* it consumes the stream (08 — `withTurnHeartbeat`); the FIRST beat arms the
|
|
10
|
+
* watchdog, and from then on `IDLE_ABORT_MS` of silence aborts the turn.
|
|
11
|
+
* Arming is opt-in by construction: consumers that never beat (curl drills,
|
|
12
|
+
* scripted clients, older panels) keep run-to-completion semantics.
|
|
13
|
+
*
|
|
14
|
+
* The registry lives on globalThis (Symbol.for) so HMR copies of this module
|
|
15
|
+
* under a dev server share one view: a turn registered before an edit is still
|
|
16
|
+
* beatable after it.
|
|
17
|
+
*/
|
|
18
|
+
/** Track one streaming turn; returns its unregister. Registration alone never
|
|
19
|
+
* arms the watchdog — only a first heartbeat does. */
|
|
20
|
+
export declare function registerActiveTurn(turn: {
|
|
21
|
+
threadId: string;
|
|
22
|
+
subject: string;
|
|
23
|
+
abort: () => void;
|
|
24
|
+
}): () => void;
|
|
25
|
+
/** A heartbeat for `threadId` from `subject`. Refreshes (and on first beat
|
|
26
|
+
* arms) the idle watchdog of every matching in-flight turn. Foreign or
|
|
27
|
+
* unknown ids answer false — no oracle, and a beat can never keep (or end)
|
|
28
|
+
* another principal's turn. */
|
|
29
|
+
export declare function touchActiveTurn(threadId: string, subject: string): boolean;
|
|
30
|
+
/** Wrap a turn response so `onSettled` runs exactly once when its stream
|
|
31
|
+
* finishes, errors, or is cancelled — the turn's registry entry must not
|
|
32
|
+
* outlive the stream. Mirrors the wire's inflight-bracket wrapper. */
|
|
33
|
+
export declare function trackTurnResponse(response: Response, onSettled: () => void): Response;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** ENG-353 — server-side turn liveness: the idle-abort fallback for client
|
|
2
|
+
* disconnects the runtime never surfaces.
|
|
3
|
+
*
|
|
4
|
+
* The fast path is unchanged: `request.signal` cancels the turn the moment the
|
|
5
|
+
* runtime propagates a fetch abort (wave-5 AGENT-3). But under `next dev` a
|
|
6
|
+
* real browser's graceful tab-close/navigate-away fires neither the signal nor
|
|
7
|
+
* a stream cancel, so an abandoned turn runs to completion. The fallback is
|
|
8
|
+
* liveness by heartbeat: the panel beats `POST /threads/:id/heartbeat` while
|
|
9
|
+
* it consumes the stream (08 — `withTurnHeartbeat`); the FIRST beat arms the
|
|
10
|
+
* watchdog, and from then on `IDLE_ABORT_MS` of silence aborts the turn.
|
|
11
|
+
* Arming is opt-in by construction: consumers that never beat (curl drills,
|
|
12
|
+
* scripted clients, older panels) keep run-to-completion semantics.
|
|
13
|
+
*
|
|
14
|
+
* The registry lives on globalThis (Symbol.for) so HMR copies of this module
|
|
15
|
+
* under a dev server share one view: a turn registered before an edit is still
|
|
16
|
+
* beatable after it.
|
|
17
|
+
*/
|
|
18
|
+
import { environment } from "./wire/shared.js";
|
|
19
|
+
const IDLE_ABORT_MS = 15_000;
|
|
20
|
+
const ACTIVE_TURNS_KEY = Symbol.for("vendoai.vendo.active-turns@1");
|
|
21
|
+
function activeTurns() {
|
|
22
|
+
const holder = globalThis;
|
|
23
|
+
return (holder[ACTIVE_TURNS_KEY] ??= new Set());
|
|
24
|
+
}
|
|
25
|
+
/** Test seam only: the idle window, overridable per call site via env. The
|
|
26
|
+
* process-guarded `environment` helper keeps every beat working on
|
|
27
|
+
* edge/Worker targets with no `process` global. */
|
|
28
|
+
function idleAbortMs() {
|
|
29
|
+
const configured = Number(environment("VENDO_TURN_IDLE_ABORT_MS"));
|
|
30
|
+
return Number.isFinite(configured) && configured > 0 ? configured : IDLE_ABORT_MS;
|
|
31
|
+
}
|
|
32
|
+
/** Track one streaming turn; returns its unregister. Registration alone never
|
|
33
|
+
* arms the watchdog — only a first heartbeat does. */
|
|
34
|
+
export function registerActiveTurn(turn) {
|
|
35
|
+
const entry = { ...turn };
|
|
36
|
+
activeTurns().add(entry);
|
|
37
|
+
return () => {
|
|
38
|
+
if (entry.idleTimer !== undefined)
|
|
39
|
+
clearTimeout(entry.idleTimer);
|
|
40
|
+
activeTurns().delete(entry);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** A heartbeat for `threadId` from `subject`. Refreshes (and on first beat
|
|
44
|
+
* arms) the idle watchdog of every matching in-flight turn. Foreign or
|
|
45
|
+
* unknown ids answer false — no oracle, and a beat can never keep (or end)
|
|
46
|
+
* another principal's turn. */
|
|
47
|
+
export function touchActiveTurn(threadId, subject) {
|
|
48
|
+
let active = false;
|
|
49
|
+
for (const turn of activeTurns()) {
|
|
50
|
+
if (turn.threadId !== threadId || turn.subject !== subject)
|
|
51
|
+
continue;
|
|
52
|
+
active = true;
|
|
53
|
+
if (turn.idleTimer !== undefined)
|
|
54
|
+
clearTimeout(turn.idleTimer);
|
|
55
|
+
turn.idleTimer = setTimeout(() => {
|
|
56
|
+
console.warn(`[vendo] turn on thread ${turn.threadId} lost its client heartbeat for ${idleAbortMs()}ms — aborting the abandoned turn.`);
|
|
57
|
+
// Drop the entry now (idempotent with the stream-settled unregister):
|
|
58
|
+
// an idle-aborted turn is over, and a late beat must see it inactive
|
|
59
|
+
// even before the runtime drains the closing stream.
|
|
60
|
+
activeTurns().delete(turn);
|
|
61
|
+
turn.abort();
|
|
62
|
+
}, idleAbortMs());
|
|
63
|
+
turn.idleTimer.unref?.();
|
|
64
|
+
}
|
|
65
|
+
return active;
|
|
66
|
+
}
|
|
67
|
+
/** Wrap a turn response so `onSettled` runs exactly once when its stream
|
|
68
|
+
* finishes, errors, or is cancelled — the turn's registry entry must not
|
|
69
|
+
* outlive the stream. Mirrors the wire's inflight-bracket wrapper. */
|
|
70
|
+
export function trackTurnResponse(response, onSettled) {
|
|
71
|
+
if (response.body === null) {
|
|
72
|
+
onSettled();
|
|
73
|
+
return response;
|
|
74
|
+
}
|
|
75
|
+
let settled = false;
|
|
76
|
+
const settle = () => {
|
|
77
|
+
if (settled)
|
|
78
|
+
return;
|
|
79
|
+
settled = true;
|
|
80
|
+
onSettled();
|
|
81
|
+
};
|
|
82
|
+
const reader = response.body.getReader();
|
|
83
|
+
const tracked = new ReadableStream({
|
|
84
|
+
async pull(controller) {
|
|
85
|
+
try {
|
|
86
|
+
const { done, value } = await reader.read();
|
|
87
|
+
if (done) {
|
|
88
|
+
settle();
|
|
89
|
+
controller.close();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
controller.enqueue(value);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
settle();
|
|
96
|
+
controller.error(error);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
cancel(reason) {
|
|
100
|
+
settle();
|
|
101
|
+
return reader.cancel(reason);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
return new Response(tracked, response);
|
|
105
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type RouteEntry } from "./shared.js";
|
|
2
|
+
/** 05-guard / 09 §3 — the approvals wire area. Org-scoped approvals
|
|
3
|
+
(`?org=<id>` / body `org`) were a Vendo Cloud capability (block-actions
|
|
4
|
+
design §C); orgs are cut from OSS (kill-list A5), so a request carrying an
|
|
5
|
+
org param here now gets the same cloud-required error the /orgs routes
|
|
6
|
+
answer, rather than silently ignoring it. */
|
|
7
|
+
export declare const approvalRoutes: RouteEntry[];
|
|
8
|
+
/** Same cloud-required `?org=` scoping as approvals: standing grants scoping
|
|
9
|
+
to an org is a Vendo Cloud capability. */
|
|
10
|
+
export declare const grantRoutes: RouteEntry[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { VendoError, approvalDecisionSchema } from "@vendoai/core";
|
|
2
|
+
import { json, orgsCloudRequired, requestJson, route, string } from "./shared.js";
|
|
3
|
+
/** 05-guard / 09 §3 — the approvals wire area. Org-scoped approvals
|
|
4
|
+
(`?org=<id>` / body `org`) were a Vendo Cloud capability (block-actions
|
|
5
|
+
design §C); orgs are cut from OSS (kill-list A5), so a request carrying an
|
|
6
|
+
org param here now gets the same cloud-required error the /orgs routes
|
|
7
|
+
answer, rather than silently ignoring it. */
|
|
8
|
+
export const approvalRoutes = [
|
|
9
|
+
route("GET", "/approvals", async ({ url, deps, context }) => {
|
|
10
|
+
const ctx = await context("chat");
|
|
11
|
+
if (url.searchParams.get("org") !== null)
|
|
12
|
+
orgsCloudRequired();
|
|
13
|
+
return json(await deps.guard.approvals.pending(ctx.principal));
|
|
14
|
+
}),
|
|
15
|
+
// Existing-agents Lane B — the read `<VendoApprovalEmbed>` polls for a
|
|
16
|
+
// parked BYO guarded call: the frozen VendoApprovalEmbedState vocabulary,
|
|
17
|
+
// carrying the full request while pending (the consent card shows real
|
|
18
|
+
// inputs) and the resumed call's outcome once executed. Owner-scoped;
|
|
19
|
+
// unknown and foreign ids both answer not-found. Registered before the
|
|
20
|
+
// decide route only textually — decide's exact-path POST never collides
|
|
21
|
+
// with this GET segment pattern.
|
|
22
|
+
route("GET", "/approvals/:id", async ({ url, deps, context, params }) => {
|
|
23
|
+
const ctx = await context("chat");
|
|
24
|
+
if (url.searchParams.get("org") !== null)
|
|
25
|
+
orgsCloudRequired();
|
|
26
|
+
return json(await deps.byoApprovals.read(string(params["id"], "approval id"), ctx.principal));
|
|
27
|
+
}),
|
|
28
|
+
route("POST", "/approvals/decide", async ({ request, deps, context }) => {
|
|
29
|
+
const body = await requestJson(request);
|
|
30
|
+
const ids = Array.isArray(body["ids"]) ? body["ids"].map((id) => string(id, "approval id")) : [];
|
|
31
|
+
if (ids.length === 0)
|
|
32
|
+
throw new VendoError("validation", "ids must contain at least one approval id");
|
|
33
|
+
const decision = approvalDecisionSchema.safeParse(body["decision"]);
|
|
34
|
+
if (!decision.success)
|
|
35
|
+
throw new VendoError("validation", "decision is invalid");
|
|
36
|
+
const ctx = await context("chat");
|
|
37
|
+
if (body["org"] !== undefined)
|
|
38
|
+
orgsCloudRequired();
|
|
39
|
+
await deps.guard.approvals.decide(ids, decision.data, ctx.principal);
|
|
40
|
+
return json({});
|
|
41
|
+
}),
|
|
42
|
+
];
|
|
43
|
+
/** Same cloud-required `?org=` scoping as approvals: standing grants scoping
|
|
44
|
+
to an org is a Vendo Cloud capability. */
|
|
45
|
+
export const grantRoutes = [
|
|
46
|
+
route("GET", "/grants", async ({ url, deps, context }) => {
|
|
47
|
+
const ctx = await context("chat");
|
|
48
|
+
if (url.searchParams.get("org") !== null)
|
|
49
|
+
orgsCloudRequired();
|
|
50
|
+
return json(await deps.guard.grants.list(ctx.principal));
|
|
51
|
+
}),
|
|
52
|
+
route("DELETE", "/grants/:id", async ({ url, deps, context, params }) => {
|
|
53
|
+
const ctx = await context("chat");
|
|
54
|
+
if (url.searchParams.get("org") !== null)
|
|
55
|
+
orgsCloudRequired();
|
|
56
|
+
await deps.guard.grants.revoke(string(params["id"], "grant id"), ctx.principal);
|
|
57
|
+
return json({});
|
|
58
|
+
}),
|
|
59
|
+
];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type RouteEntry } from "./shared.js";
|
|
2
|
+
/** 06-apps / 09 §3 — the /apps wire area: CRUD, open/call/edit, history,
|
|
3
|
+
ship-diff, pin drift/rebase, the gesture fork (fork-pin), export/import,
|
|
4
|
+
fork (whole-app copy — a different feature from fork-pin). */
|
|
5
|
+
export declare const appRoutes: RouteEntry[];
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { VendoError } from "@vendoai/core";
|
|
2
|
+
import { json, requestJson, route, string } from "./shared.js";
|
|
3
|
+
/** 06-apps / 09 §3 — the /apps wire area: CRUD, open/call/edit, history,
|
|
4
|
+
ship-diff, pin drift/rebase, the gesture fork (fork-pin), export/import,
|
|
5
|
+
fork (whole-app copy — a different feature from fork-pin). */
|
|
6
|
+
export const appRoutes = [
|
|
7
|
+
// Grouped like the old if-chain arm: ANY method on /apps resolves context
|
|
8
|
+
// first; an unhandled method falls through to the table's not-found.
|
|
9
|
+
route("*", "/apps", async ({ request, deps, context }) => {
|
|
10
|
+
const ctx = await context("app");
|
|
11
|
+
if (request.method === "GET") {
|
|
12
|
+
return json(await deps.apps.list(ctx));
|
|
13
|
+
}
|
|
14
|
+
if (request.method === "POST") {
|
|
15
|
+
const body = await requestJson(request);
|
|
16
|
+
return json(await deps.apps.create({ prompt: string(body["prompt"], "prompt") }, ctx));
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}),
|
|
20
|
+
// 06-apps §8 — gesture-owned forking (2026-07-21): the deterministic fork
|
|
21
|
+
// the user's Remix gesture on an EMPTY slot invokes. The engine mints a
|
|
22
|
+
// minimal app, copies the captured baseline, and records the pin — no model
|
|
23
|
+
// call. An optional instruction then rides the ordinary edit path, already
|
|
24
|
+
// scoped to the forked component. The model never decides to fork.
|
|
25
|
+
// ORDER IS LOAD-BEARING: this entry (and /apps/import below) must stay
|
|
26
|
+
// ahead of the "/apps/:appId/*" catch-all, whose rest pattern would
|
|
27
|
+
// otherwise capture appId="fork-pin".
|
|
28
|
+
route("POST", "/apps/fork-pin", async ({ request, deps, context }) => {
|
|
29
|
+
const ctx = await context("app");
|
|
30
|
+
const body = await requestJson(request);
|
|
31
|
+
return json(await deps.apps.pins.fork({
|
|
32
|
+
slot: string(body["slot"], "slot"),
|
|
33
|
+
...(body["instruction"] === undefined ? {} : { instruction: string(body["instruction"], "instruction") }),
|
|
34
|
+
}, ctx));
|
|
35
|
+
}),
|
|
36
|
+
route("POST", "/apps/import", async ({ request, deps, context }) => {
|
|
37
|
+
// The CSRF floor exempts import (binary body), so it must instead require
|
|
38
|
+
// a non-CORS-safelisted media type — forcing a cross-origin preflight so
|
|
39
|
+
// a simple credentialed form/text POST cannot silently import (09 §3).
|
|
40
|
+
const contentType = request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
41
|
+
if (contentType !== "application/octet-stream" && contentType !== "application/vnd.vendo.app") {
|
|
42
|
+
throw new VendoError("validation", "import requires Content-Type: application/octet-stream");
|
|
43
|
+
}
|
|
44
|
+
const ctx = await context("app");
|
|
45
|
+
return json(await deps.apps.importApp(new Uint8Array(await request.arrayBuffer()), ctx));
|
|
46
|
+
}),
|
|
47
|
+
// The old `head === "apps" && segments.length >= 2` grouped arm, verbatim:
|
|
48
|
+
// context resolves for ANY /apps/:appId[/...] request before the method and
|
|
49
|
+
// operation checks, and an unmatched combination falls through to not-found.
|
|
50
|
+
route("*", "/apps/:appId/*", async (wire) => {
|
|
51
|
+
const { request, deps, params, segments } = wire;
|
|
52
|
+
const appId = string(params["appId"], "app id");
|
|
53
|
+
const ctx = await wire.context("app");
|
|
54
|
+
const operation = segments[2];
|
|
55
|
+
if (segments.length === 2) {
|
|
56
|
+
if (request.method === "GET") {
|
|
57
|
+
const app = await deps.apps.get(appId, ctx);
|
|
58
|
+
if (app === null)
|
|
59
|
+
throw new VendoError("not-found", `app not found: ${appId}`);
|
|
60
|
+
return json(app);
|
|
61
|
+
}
|
|
62
|
+
if (request.method === "DELETE") {
|
|
63
|
+
await deps.apps.delete(appId, ctx);
|
|
64
|
+
return json({});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (request.method === "GET" && operation === "open" && segments.length === 3) {
|
|
68
|
+
// Existing-agents polish — the embed's build-window poll. The app record
|
|
69
|
+
// lands only at build completion, so until then open() (and the meta
|
|
70
|
+
// route alike) answers not-found, and every 1.2s poll logged a browser
|
|
71
|
+
// console 404. Under the additive ?pending=1 flag, ONLY that expected
|
|
72
|
+
// pre-servable miss becomes a quiet 200 {kind:"pending"}; unflagged
|
|
73
|
+
// callers keep the contracted 404, and every other failure keeps its
|
|
74
|
+
// envelope and status either way.
|
|
75
|
+
if (wire.url.searchParams.get("pending") === "1") {
|
|
76
|
+
try {
|
|
77
|
+
return json(await deps.apps.open(appId, ctx));
|
|
78
|
+
}
|
|
79
|
+
catch (reason) {
|
|
80
|
+
if (reason instanceof VendoError && reason.code === "not-found") {
|
|
81
|
+
return json({ kind: "pending" });
|
|
82
|
+
}
|
|
83
|
+
throw reason;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return json(await deps.apps.open(appId, ctx));
|
|
87
|
+
}
|
|
88
|
+
if (request.method === "POST" && operation === "call" && segments.length === 3) {
|
|
89
|
+
const body = await requestJson(request);
|
|
90
|
+
return json(await deps.apps.call(appId, string(body["ref"], "ref"), body["args"], ctx));
|
|
91
|
+
}
|
|
92
|
+
if (request.method === "POST" && operation === "edit" && segments.length === 3) {
|
|
93
|
+
const body = await requestJson(request);
|
|
94
|
+
return json(await deps.apps.edit(appId, string(body["instruction"], "instruction"), ctx));
|
|
95
|
+
}
|
|
96
|
+
if (operation === "history" && segments.length === 3) {
|
|
97
|
+
if (await deps.apps.get(appId, ctx) === null)
|
|
98
|
+
throw new VendoError("not-found", `app not found: ${appId}`);
|
|
99
|
+
if (request.method === "GET")
|
|
100
|
+
return json(await deps.apps.history(appId).list());
|
|
101
|
+
if (request.method === "POST") {
|
|
102
|
+
const body = await requestJson(request);
|
|
103
|
+
if (body["op"] !== "undo")
|
|
104
|
+
throw new VendoError("validation", "history op must be undo");
|
|
105
|
+
return json(await deps.apps.history(appId).undo());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// 06-apps §8–§9 — additive: the reviewable diff of what this app ships
|
|
109
|
+
// relative to the captured host baselines, hash-pinned to the version
|
|
110
|
+
// an in-client approval would cover. Owner-scoped like every app route.
|
|
111
|
+
if (request.method === "GET" && operation === "ship-diff" && segments.length === 3) {
|
|
112
|
+
return json(await deps.apps.inClient.shipDiff(appId, ctx));
|
|
113
|
+
}
|
|
114
|
+
// 06-apps §8 — additive drift→rebase surface, owner-scoped like every
|
|
115
|
+
// app route. A rebase rewrites content, so it is only ever invoked
|
|
116
|
+
// explicitly here or via the vendo_apps_rebase_pin agent tool — drift
|
|
117
|
+
// detection never auto-rebases.
|
|
118
|
+
if (request.method === "GET" && operation === "pin-drift" && segments.length === 3) {
|
|
119
|
+
return json(await deps.apps.pins.drift(appId, ctx));
|
|
120
|
+
}
|
|
121
|
+
if (request.method === "POST" && operation === "rebase-pin" && segments.length === 3) {
|
|
122
|
+
const body = await requestJson(request);
|
|
123
|
+
return json(await deps.apps.pins.rebase({ appId, slot: string(body["slot"], "slot") }, ctx));
|
|
124
|
+
}
|
|
125
|
+
// 06-apps §8 — the same gesture fork landing in an EXISTING app (the
|
|
126
|
+
// filled-slot / driver surface). Owner-scoped like every app route.
|
|
127
|
+
if (request.method === "POST" && operation === "fork-pin" && segments.length === 3) {
|
|
128
|
+
const body = await requestJson(request);
|
|
129
|
+
return json(await deps.apps.pins.fork({
|
|
130
|
+
appId,
|
|
131
|
+
slot: string(body["slot"], "slot"),
|
|
132
|
+
...(body["instruction"] === undefined ? {} : { instruction: string(body["instruction"], "instruction") }),
|
|
133
|
+
}, ctx));
|
|
134
|
+
}
|
|
135
|
+
// Wave 7 H2 — the embed surface's keepalive: user activity on an embedded
|
|
136
|
+
// served app rides one host-proxied HEAD through the machine (re-arming
|
|
137
|
+
// the idle timer); "woke" tells the embed its URL is stale — re-open.
|
|
138
|
+
if (request.method === "POST" && operation === "machine" && segments[3] === "ping" && segments.length === 4) {
|
|
139
|
+
return json(await deps.apps.machine.ping(appId, ctx));
|
|
140
|
+
}
|
|
141
|
+
if (request.method === "GET" && operation === "export" && segments.length === 3) {
|
|
142
|
+
const bytes = await deps.apps.exportApp(appId, ctx);
|
|
143
|
+
return new Response(bytes, {
|
|
144
|
+
headers: {
|
|
145
|
+
"content-type": "application/octet-stream",
|
|
146
|
+
"content-disposition": `attachment; filename="${appId}.vendoapp"`,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (request.method === "POST" && operation === "fork" && segments.length === 3) {
|
|
151
|
+
return json(await deps.apps.fork(appId, ctx));
|
|
152
|
+
}
|
|
153
|
+
return undefined;
|
|
154
|
+
}),
|
|
155
|
+
];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { VendoError } from "@vendoai/core";
|
|
2
|
+
import { json, route, string } from "./shared.js";
|
|
3
|
+
/** 07-automations / 09 §3 — the /automations wire area. */
|
|
4
|
+
export const automationRoutes = [
|
|
5
|
+
route("GET", "/automations", async ({ deps, context }) => {
|
|
6
|
+
return json(await deps.automations.list(await context("automation")));
|
|
7
|
+
}),
|
|
8
|
+
// Grouped like the old if-chain arm (`segments.length === 3 && POST`):
|
|
9
|
+
// context resolves before the operation check, and an unknown operation
|
|
10
|
+
// falls through to the table's not-found.
|
|
11
|
+
route("POST", "/automations/:appId/:op", async ({ deps, context, params, segments }) => {
|
|
12
|
+
const appId = string(params["appId"], "app id");
|
|
13
|
+
const ctx = await context("automation");
|
|
14
|
+
if (segments[2] === "enable")
|
|
15
|
+
return json(await deps.automations.enable(appId, ctx));
|
|
16
|
+
if (segments[2] === "disable") {
|
|
17
|
+
await deps.automations.disable(appId, ctx);
|
|
18
|
+
return json({});
|
|
19
|
+
}
|
|
20
|
+
if (segments[2] === "dry-run")
|
|
21
|
+
return json(await deps.automations.dryRun(appId, ctx));
|
|
22
|
+
return undefined;
|
|
23
|
+
}),
|
|
24
|
+
];
|
|
25
|
+
/** 07-automations / 09 §3 — the /runs wire area. */
|
|
26
|
+
export const runRoutes = [
|
|
27
|
+
route("GET", "/runs", async ({ url, deps, context }) => {
|
|
28
|
+
const status = url.searchParams.get("status") ?? undefined;
|
|
29
|
+
const allowed = ["running", "ok", "error", "stopped", "pending-approval"];
|
|
30
|
+
if (status !== undefined && !allowed.includes(status)) {
|
|
31
|
+
throw new VendoError("validation", "run status is invalid");
|
|
32
|
+
}
|
|
33
|
+
const filter = {
|
|
34
|
+
...(url.searchParams.get("appId") === null ? {} : { appId: url.searchParams.get("appId") }),
|
|
35
|
+
...(status === undefined ? {} : { status: status }),
|
|
36
|
+
...(url.searchParams.get("cursor") === null ? {} : { cursor: url.searchParams.get("cursor") }),
|
|
37
|
+
};
|
|
38
|
+
return json(await deps.automations.runs.list(filter, await context("automation")));
|
|
39
|
+
}),
|
|
40
|
+
// Grouped like the old `head === "runs" && segments.length >= 2` arm: ANY
|
|
41
|
+
// method/depth resolves context first; unmatched shapes fall through.
|
|
42
|
+
route("*", "/runs/:runId/*", async ({ request, deps, context, params, segments }) => {
|
|
43
|
+
const ctx = await context("automation");
|
|
44
|
+
const runId = string(params["runId"], "run id");
|
|
45
|
+
if (request.method === "GET" && segments.length === 2) {
|
|
46
|
+
const run = await deps.automations.runs.get(runId, ctx);
|
|
47
|
+
if (run === null)
|
|
48
|
+
throw new VendoError("not-found", `run not found: ${runId}`);
|
|
49
|
+
return json(run);
|
|
50
|
+
}
|
|
51
|
+
if (request.method === "POST" && segments[2] === "stop" && segments.length === 3) {
|
|
52
|
+
await deps.automations.runs.stop(runId, ctx);
|
|
53
|
+
return json({});
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}),
|
|
57
|
+
];
|