@intentic/sandbox-contract 1.306.0 → 1.308.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 +12 -0
- package/dist/contracts/accounts.contract.d.ts +6 -6
- package/dist/contracts/accounts.contract.js +7 -7
- package/dist/contracts/accounts.contract.js.map +1 -1
- package/dist/contracts/activity.contract.d.ts +2 -2
- package/dist/contracts/activity.contract.js +3 -3
- package/dist/contracts/activity.contract.js.map +1 -1
- package/dist/contracts/agent.contract.d.ts +10 -10
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +21 -11
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +41 -27
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +44 -28
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/approvals.contract.d.ts +3 -3
- package/dist/contracts/approvals.contract.js +4 -4
- package/dist/contracts/approvals.contract.js.map +1 -1
- package/dist/contracts/areas.contract.d.ts +3 -3
- package/dist/contracts/areas.contract.d.ts.map +1 -1
- package/dist/contracts/areas.contract.js +5 -4
- package/dist/contracts/areas.contract.js.map +1 -1
- package/dist/contracts/automations.contract.d.ts +11 -11
- package/dist/contracts/automations.contract.js +12 -12
- package/dist/contracts/automations.contract.js.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +13 -13
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/capabilities.contract.js +18 -14
- package/dist/contracts/capabilities.contract.js.map +1 -1
- package/dist/contracts/chores.contract.d.ts +3 -3
- package/dist/contracts/chores.contract.js +4 -4
- package/dist/contracts/chores.contract.js.map +1 -1
- package/dist/contracts/ci.contract.d.ts +5 -5
- package/dist/contracts/ci.contract.js +6 -6
- package/dist/contracts/ci.contract.js.map +1 -1
- package/dist/contracts/diff.contract.d.ts +1 -1
- package/dist/contracts/diff.contract.js +2 -2
- package/dist/contracts/diff.contract.js.map +1 -1
- package/dist/contracts/endpoints.contract.d.ts +4 -4
- package/dist/contracts/endpoints.contract.js +5 -5
- package/dist/contracts/endpoints.contract.js.map +1 -1
- package/dist/contracts/exit.contract.d.ts +7 -7
- package/dist/contracts/exit.contract.d.ts.map +1 -1
- package/dist/contracts/exit.contract.js +9 -8
- package/dist/contracts/exit.contract.js.map +1 -1
- package/dist/contracts/extensions.contract.d.ts +17 -17
- package/dist/contracts/extensions.contract.js +18 -18
- package/dist/contracts/extensions.contract.js.map +1 -1
- package/dist/contracts/git.contract.d.ts +47 -45
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.js +46 -46
- package/dist/contracts/git.contract.js.map +1 -1
- package/dist/contracts/history.contract.d.ts +4 -4
- package/dist/contracts/history.contract.js +5 -5
- package/dist/contracts/history.contract.js.map +1 -1
- package/dist/contracts/intentic.contract.d.ts +3 -3
- package/dist/contracts/intentic.contract.d.ts.map +1 -1
- package/dist/contracts/intentic.contract.js +5 -4
- package/dist/contracts/intentic.contract.js.map +1 -1
- package/dist/contracts/inventory.contract.d.ts +3 -3
- package/dist/contracts/inventory.contract.js +4 -4
- package/dist/contracts/inventory.contract.js.map +1 -1
- package/dist/contracts/issues.contract.d.ts +5 -5
- package/dist/contracts/issues.contract.js +6 -6
- package/dist/contracts/issues.contract.js.map +1 -1
- package/dist/contracts/logs.contract.d.ts +3 -3
- package/dist/contracts/logs.contract.d.ts.map +1 -1
- package/dist/contracts/logs.contract.js +6 -4
- package/dist/contracts/logs.contract.js.map +1 -1
- package/dist/contracts/loops.contract.d.ts +6 -6
- package/dist/contracts/loops.contract.js +7 -7
- package/dist/contracts/loops.contract.js.map +1 -1
- package/dist/contracts/netdisk.contract.d.ts +3 -3
- package/dist/contracts/netdisk.contract.d.ts.map +1 -1
- package/dist/contracts/netdisk.contract.js +5 -4
- package/dist/contracts/netdisk.contract.js.map +1 -1
- package/dist/contracts/panels.contract.d.ts +3 -3
- package/dist/contracts/panels.contract.js +4 -4
- package/dist/contracts/panels.contract.js.map +1 -1
- package/dist/contracts/personas.contract.d.ts +8 -8
- package/dist/contracts/personas.contract.d.ts.map +1 -1
- package/dist/contracts/personas.contract.js +10 -9
- package/dist/contracts/personas.contract.js.map +1 -1
- package/dist/contracts/ports.contract.d.ts +3 -3
- package/dist/contracts/ports.contract.d.ts.map +1 -1
- package/dist/contracts/ports.contract.js +5 -4
- package/dist/contracts/ports.contract.js.map +1 -1
- package/dist/contracts/prepush.contract.d.ts +3 -3
- package/dist/contracts/prepush.contract.js +4 -4
- package/dist/contracts/prepush.contract.js.map +1 -1
- package/dist/contracts/providers.contract.d.ts +2 -2
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/providers.contract.js +5 -3
- package/dist/contracts/providers.contract.js.map +1 -1
- package/dist/contracts/public.contract.d.ts +3 -3
- package/dist/contracts/public.contract.js +4 -4
- package/dist/contracts/public.contract.js.map +1 -1
- package/dist/contracts/push.contract.d.ts +4 -4
- package/dist/contracts/push.contract.d.ts.map +1 -1
- package/dist/contracts/push.contract.js +9 -5
- package/dist/contracts/push.contract.js.map +1 -1
- package/dist/contracts/safety.contract.d.ts +3 -3
- package/dist/contracts/safety.contract.js +4 -4
- package/dist/contracts/safety.contract.js.map +1 -1
- package/dist/contracts/secrets.contract.d.ts +9 -9
- package/dist/contracts/secrets.contract.d.ts.map +1 -1
- package/dist/contracts/secrets.contract.js +13 -10
- package/dist/contracts/secrets.contract.js.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +2 -2
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/sessions.contract.js +5 -3
- package/dist/contracts/sessions.contract.js.map +1 -1
- package/dist/contracts/settings.contract.d.ts +11 -9
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.js +11 -10
- package/dist/contracts/settings.contract.js.map +1 -1
- package/dist/contracts/share.contract.d.ts +4 -4
- package/dist/contracts/share.contract.js +5 -5
- package/dist/contracts/share.contract.js.map +1 -1
- package/dist/contracts/skills.contract.d.ts +5 -5
- package/dist/contracts/skills.contract.js +6 -6
- package/dist/contracts/skills.contract.js.map +1 -1
- package/dist/contracts/system.contract.d.ts +22 -19
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.js +27 -20
- package/dist/contracts/system.contract.js.map +1 -1
- package/dist/contracts/translator.contract.d.ts +5 -5
- package/dist/contracts/translator.contract.js +6 -6
- package/dist/contracts/translator.contract.js.map +1 -1
- package/dist/contracts/usage.contract.d.ts +4 -4
- package/dist/contracts/usage.contract.js +5 -5
- package/dist/contracts/usage.contract.js.map +1 -1
- package/dist/contracts/vpn.contract.d.ts +4 -4
- package/dist/contracts/vpn.contract.d.ts.map +1 -1
- package/dist/contracts/vpn.contract.js +6 -5
- package/dist/contracts/vpn.contract.js.map +1 -1
- package/dist/contracts/workflows.contract.d.ts +9 -9
- package/dist/contracts/workflows.contract.js +10 -10
- package/dist/contracts/workflows.contract.js.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +30 -30
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.js +46 -31
- package/dist/contracts/workspace.contract.js.map +1 -1
- package/dist/events/land-breakage.d.ts +4 -0
- package/dist/events/land-breakage.d.ts.map +1 -0
- package/dist/events/land-breakage.js +4 -0
- package/dist/events/land-breakage.js.map +1 -0
- package/dist/events/system-events.d.ts +2 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/index.d.ts +9379 -9350
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol/procedure-types.d.ts +10 -0
- package/dist/protocol/procedure-types.d.ts.map +1 -0
- package/dist/protocol/procedure-types.js +2 -0
- package/dist/protocol/procedure-types.js.map +1 -0
- package/dist/protocol/raw-routes.d.ts +417 -0
- package/dist/protocol/raw-routes.d.ts.map +1 -0
- package/dist/protocol/raw-routes.js +140 -0
- package/dist/protocol/raw-routes.js.map +1 -0
- package/dist/protocol/route-meta.d.ts +18 -0
- package/dist/protocol/route-meta.d.ts.map +1 -0
- package/dist/protocol/route-meta.js +3 -0
- package/dist/protocol/route-meta.js.map +1 -0
- package/dist/protocol/routes.d.ts +3 -0
- package/dist/protocol/routes.d.ts.map +1 -1
- package/dist/protocol/routes.js +47 -14
- package/dist/protocol/routes.js.map +1 -1
- package/dist/schemas/agent.d.ts +33 -0
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +22 -3
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents.d.ts +4 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +4 -0
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/automations.d.ts +1 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/git/git.d.ts +3 -0
- package/dist/schemas/git/git.d.ts.map +1 -1
- package/dist/schemas/inventory.d.ts +1 -0
- package/dist/schemas/inventory.d.ts.map +1 -1
- package/dist/schemas/inventory.js +1 -0
- package/dist/schemas/inventory.js.map +1 -1
- package/dist/schemas/settings.d.ts +1 -0
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +4 -0
- package/dist/schemas/settings.js.map +1 -1
- package/dist/state/definition.d.ts +4 -0
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/history-state.d.ts.map +1 -1
- package/dist/state/history-state.js +2 -12
- package/dist/state/history-state.js.map +1 -1
- package/dist/state/landed-commit.d.ts +4 -0
- package/dist/state/landed-commit.d.ts.map +1 -0
- package/dist/state/landed-commit.js +12 -0
- package/dist/state/landed-commit.js.map +1 -0
- package/dist/state/runtime-state.d.ts +6 -0
- package/dist/state/runtime-state.d.ts.map +1 -1
- package/dist/state/runtime-state.js +2 -0
- package/dist/state/runtime-state.js.map +1 -1
- package/package.json +5 -5
- package/src/contracts/accounts.contract.ts +7 -7
- package/src/contracts/activity.contract.ts +3 -3
- package/src/contracts/agent.contract.ts +25 -11
- package/src/contracts/agents.contract.ts +48 -30
- package/src/contracts/approvals.contract.ts +4 -4
- package/src/contracts/areas.contract.ts +5 -4
- package/src/contracts/automations.contract.ts +12 -12
- package/src/contracts/capabilities.contract.ts +23 -14
- package/src/contracts/chores.contract.ts +4 -4
- package/src/contracts/ci.contract.ts +6 -6
- package/src/contracts/diff.contract.ts +2 -2
- package/src/contracts/endpoints.contract.ts +5 -5
- package/src/contracts/exit.contract.ts +11 -8
- package/src/contracts/extensions.contract.ts +18 -18
- package/src/contracts/git.contract.ts +46 -46
- package/src/contracts/history.contract.ts +5 -5
- package/src/contracts/intentic.contract.ts +5 -4
- package/src/contracts/inventory.contract.ts +4 -4
- package/src/contracts/issues.contract.ts +6 -6
- package/src/contracts/logs.contract.ts +8 -4
- package/src/contracts/loops.contract.ts +7 -7
- package/src/contracts/netdisk.contract.ts +7 -4
- package/src/contracts/panels.contract.ts +4 -4
- package/src/contracts/personas.contract.ts +10 -9
- package/src/contracts/ports.contract.ts +6 -4
- package/src/contracts/prepush.contract.ts +4 -4
- package/src/contracts/providers.contract.ts +5 -3
- package/src/contracts/public.contract.ts +4 -4
- package/src/contracts/push.contract.ts +12 -5
- package/src/contracts/safety.contract.ts +4 -4
- package/src/contracts/secrets.contract.ts +16 -10
- package/src/contracts/sessions.contract.ts +6 -3
- package/src/contracts/settings.contract.ts +11 -10
- package/src/contracts/share.contract.ts +5 -5
- package/src/contracts/skills.contract.ts +6 -6
- package/src/contracts/system.contract.ts +32 -20
- package/src/contracts/translator.contract.ts +6 -6
- package/src/contracts/usage.contract.ts +5 -5
- package/src/contracts/vpn.contract.ts +8 -5
- package/src/contracts/workflows.contract.ts +10 -10
- package/src/contracts/workspace.contract.ts +49 -31
- package/src/events/land-breakage.ts +11 -0
- package/src/index.ts +23 -1
- package/src/models/capability-ledger.test.ts +1 -1
- package/src/protocol/procedure-types.ts +24 -0
- package/src/protocol/raw-routes.ts +191 -0
- package/src/protocol/route-meta.ts +39 -0
- package/src/protocol/routes.test.ts +65 -2
- package/src/protocol/routes.ts +88 -27
- package/src/schemas/agent.test.ts +66 -0
- package/src/schemas/agent.ts +184 -154
- package/src/schemas/agents.ts +6 -0
- package/src/schemas/inventory.ts +3 -0
- package/src/schemas/settings.ts +9 -0
- package/src/state/history-state.ts +14 -29
- package/src/state/landed-commit.test.ts +18 -0
- package/src/state/landed-commit.ts +17 -0
- package/src/state/runtime-state.test.ts +9 -0
- package/src/state/runtime-state.ts +6 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import type { MemberRole } from "../schemas/shared.js";
|
|
3
|
+
|
|
4
|
+
// Every route's policy, declared beside it: how a request authenticates, which member tier and which machine
|
|
5
|
+
// credentials reach it, and how the daemon's outer middleware treats it. Each field has a default, so most routes
|
|
6
|
+
// declare nothing; the daemon's bearer, boot, timer, role-floor and grant decisions all derive from this.
|
|
7
|
+
|
|
8
|
+
// How far a control token reaches a route beyond what its floor alone gives the `read`/`drive` rungs.
|
|
9
|
+
// `never`: no rung; `editor`: the editor slice too; `read`: the read rung, though no GET; `land`: the land rung too.
|
|
10
|
+
export type ControlReach = "never" | "editor" | "read" | "land";
|
|
11
|
+
|
|
12
|
+
export interface RouteMeta {
|
|
13
|
+
// `door`: the session middleware lets it through, and the handler checks its own credential or none.
|
|
14
|
+
readonly auth?: "session" | "door";
|
|
15
|
+
// Loaded by third-party pages: CORS reflects the caller's origin, and the route's own allowlist is the gate.
|
|
16
|
+
readonly embedded?: true;
|
|
17
|
+
// Answers while the boot chain is still converging; everything else waits for it.
|
|
18
|
+
readonly beforeBoot?: true;
|
|
19
|
+
// Held open by design, so the request timer leaves it out.
|
|
20
|
+
readonly stream?: true;
|
|
21
|
+
// The lowest member role that may call it. Absent: viewer for a read (GET, HEAD), maintainer for anything else.
|
|
22
|
+
readonly floor?: MemberRole;
|
|
23
|
+
// On a guest member's allowlist, which is the whole of what a guest reaches.
|
|
24
|
+
readonly guest?: true;
|
|
25
|
+
// The floor an upload aimed inside ATTACHMENTS_DIR gets instead, one a guest may also make.
|
|
26
|
+
readonly attachmentFloor?: MemberRole;
|
|
27
|
+
// A passkey-registration door: a proof with no passkey yet may pass the require-passkey policy.
|
|
28
|
+
readonly enrolment?: true;
|
|
29
|
+
// The agent token, which the CLIs on the agent's PATH carry, reaches it.
|
|
30
|
+
readonly agent?: true;
|
|
31
|
+
// The desktop-sync token reaches it; a `poll` refreshes the machine's heartbeat, the held-open `pipe` does not.
|
|
32
|
+
readonly sync?: "poll" | "pipe";
|
|
33
|
+
// Withheld from the panel token, which reaches every other route: it puts a stored credential in motion.
|
|
34
|
+
readonly panel?: false;
|
|
35
|
+
readonly control?: ControlReach;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The builder every sandbox procedure starts from, so each carries a RouteMeta and whatever it leaves out defaults.
|
|
39
|
+
export const procedure = oc.$meta<RouteMeta>({});
|
|
@@ -2,7 +2,8 @@ import { eventIterator, oc } from "@orpc/contract";
|
|
|
2
2
|
import { describe, it, expect } from "bun:test";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { SANDBOX_ROUTE_NAMES, SANDBOX_ROUTE_SHAPES, SANDBOX_ROUTES, sandboxRouteName } from "../index.js";
|
|
5
|
-
import {
|
|
5
|
+
import { procedure } from "./route-meta.js";
|
|
6
|
+
import { contractRoutes, routeNameForRequest, routeShapes, servedRoute, streamOf } from "./routes.js";
|
|
6
7
|
|
|
7
8
|
const fixture = {
|
|
8
9
|
vpn: {
|
|
@@ -27,9 +28,16 @@ describe(`contractRoutes`, () => {
|
|
|
27
28
|
name: `vpn.connect`,
|
|
28
29
|
method: `POST`,
|
|
29
30
|
path: `/vpn/{id}/connect`,
|
|
31
|
+
meta: {},
|
|
30
32
|
});
|
|
31
33
|
});
|
|
32
34
|
|
|
35
|
+
it(`carries the policy a procedure declared, merged over its builder's`, () => {
|
|
36
|
+
const operated = procedure.meta({ agent: true, control: `never` });
|
|
37
|
+
const routes = contractRoutes({ vpn: { connect: operated.route({ method: "POST", path: "/vpn/{id}/connect" }).meta({ floor: `collaborator` }) } });
|
|
38
|
+
expect(routes).toEqual([{ name: `vpn.connect`, method: `POST`, path: `/vpn/{id}/connect`, meta: { agent: true, control: `never`, floor: `collaborator` } }]);
|
|
39
|
+
});
|
|
40
|
+
|
|
33
41
|
it(`ignores non-procedure members rather than inventing routes for them`, () => {
|
|
34
42
|
expect(contractRoutes({ vpn: { list: fixture.vpn.list, NOT_A_ROUTE: { hello: true } } }).map((r) => r.name)).toEqual([`vpn.list`]);
|
|
35
43
|
});
|
|
@@ -72,15 +80,70 @@ describe(`routeNameForRequest`, () => {
|
|
|
72
80
|
expect(routeNameForRequest(routes, `POST`, `/vpn/connect`)).toBeUndefined();
|
|
73
81
|
});
|
|
74
82
|
|
|
75
|
-
it(`
|
|
83
|
+
it(`reads a trailing slash as the end of the path, never as an empty param`, () => {
|
|
76
84
|
expect(routeNameForRequest(routes, `DELETE`, `/system/terminals/`)).toBeUndefined();
|
|
77
85
|
});
|
|
78
86
|
|
|
87
|
+
// What oRPC's router (rou3) dispatches: a spelling it serves must resolve to the route it serves, or a policy
|
|
88
|
+
// keyed on the route would not hold at that spelling.
|
|
89
|
+
it(`reads the path as oRPC's router does: two trailing slashes dropped, an empty inner segment still a param`, () => {
|
|
90
|
+
expect(routeNameForRequest(routes, `GET`, `/vpn/`)).toBe(`vpn.list`);
|
|
91
|
+
expect(routeNameForRequest(routes, `GET`, `/vpn//`)).toBe(`vpn.list`);
|
|
92
|
+
expect(routeNameForRequest(routes, `GET`, `/vpn///`)).toBeUndefined();
|
|
93
|
+
expect(routeNameForRequest(routes, `POST`, `/vpn//connect`)).toBe(`vpn.connect`);
|
|
94
|
+
expect(routeNameForRequest(routes, `POST`, `/vpn/corp-gw/connect/`)).toBe(`vpn.connect`);
|
|
95
|
+
expect(routeNameForRequest(routes, `GET`, `//vpn`)).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
79
98
|
it(`returns undefined for the daemon's hand-written non-contract routes`, () => {
|
|
80
99
|
expect(routeNameForRequest(routes, `GET`, `/health`)).toBeUndefined();
|
|
81
100
|
});
|
|
82
101
|
});
|
|
83
102
|
|
|
103
|
+
describe(`servedRoute`, () => {
|
|
104
|
+
const contract = contractRoutes(fixture);
|
|
105
|
+
const raw = [
|
|
106
|
+
{ name: `GET /health`, method: `GET`, path: `/health`, meta: { auth: `door` as const } },
|
|
107
|
+
{ name: `POST /vpn/{id}/connect`, method: `POST`, path: `/vpn/{id}/connect`, meta: { auth: `door` as const } },
|
|
108
|
+
{ name: `DELETE /vpn/{id}/connect`, method: `DELETE`, path: `/vpn/{id}/connect`, meta: {} },
|
|
109
|
+
{ name: `ALL /x/*`, method: `ALL`, path: `/x/*`, meta: {} },
|
|
110
|
+
];
|
|
111
|
+
const served = (method: string, path: string) => servedRoute(raw, contract, method, path)?.name;
|
|
112
|
+
|
|
113
|
+
it(`answers with a raw route before the contract, as the daemon registers them ahead of oRPC's catch-all`, () => {
|
|
114
|
+
expect(served(`POST`, `/vpn/corp-gw/connect`)).toBe(`POST /vpn/{id}/connect`);
|
|
115
|
+
expect(served(`GET`, `/vpn`)).toBe(`vpn.list`);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Hono is strict where oRPC is not, so a raw route's variant spelling falls through to the contract.
|
|
119
|
+
it(`matches a raw route exactly: no trailing slash, no empty param`, () => {
|
|
120
|
+
expect(served(`POST`, `/vpn/corp-gw/connect/`)).toBe(`vpn.connect`);
|
|
121
|
+
expect(served(`POST`, `/vpn//connect`)).toBe(`vpn.connect`);
|
|
122
|
+
expect(served(`GET`, `/health/`)).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it(`answers HEAD with a raw GET route and every method with an ALL one, in registration order`, () => {
|
|
126
|
+
expect(served(`HEAD`, `/health`)).toBe(`GET /health`);
|
|
127
|
+
expect(served(`HEAD`, `/vpn`)).toBeUndefined();
|
|
128
|
+
expect(served(`PATCH`, `/x/a/b`)).toBe(`ALL /x/*`);
|
|
129
|
+
expect(served(`DELETE`, `/vpn/corp-gw/connect`)).toBe(`DELETE /vpn/{id}/connect`);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// The CORS middleware answers a preflight for whatever sits at the path, before any route runs.
|
|
133
|
+
it(`resolves a preflight to the most literal route at its path, whatever method that route serves`, () => {
|
|
134
|
+
expect(served(`OPTIONS`, `/vpn/corp-gw/connect`)).toBe(`POST /vpn/{id}/connect`);
|
|
135
|
+
expect(served(`OPTIONS`, `/vpn`)).toBe(`vpn.list`);
|
|
136
|
+
expect(served(`OPTIONS`, `/no/such/route`)).toBeUndefined();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it(`gives a trailing /* the segments after the prefix, never the bare prefix`, () => {
|
|
140
|
+
expect(served(`GET`, `/x/`)).toBe(`ALL /x/*`);
|
|
141
|
+
expect(served(`GET`, `/x/a`)).toBe(`ALL /x/*`);
|
|
142
|
+
expect(served(`GET`, `/x`)).toBeUndefined();
|
|
143
|
+
expect(served(`GET`, `/xy/a`)).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
84
147
|
describe(`routeShapes`, () => {
|
|
85
148
|
it(`gives the same fingerprint for the same shape, twice`, () => {
|
|
86
149
|
expect(routeShapes(shaped(z.object({ a: z.string() })))).toEqual(routeShapes(shaped(z.object({ a: z.string() }))));
|
package/src/protocol/routes.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { eventIterator } from "@orpc/contract";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import type { RouteMeta } from "./route-meta.js";
|
|
3
4
|
|
|
4
5
|
// Named route surface of the daemon's contract (`<group>.<route>`), derived automatically so nothing here is
|
|
5
6
|
// hand-maintained. The daemon advertises which routes it implements (the /events hello frame); the browser diffs that
|
|
@@ -18,29 +19,30 @@ export const streamOf = <T extends z.ZodType>(frame: T) => Object.assign(eventIt
|
|
|
18
19
|
const frameOf = (schema: unknown): z.ZodType | undefined =>
|
|
19
20
|
typeof schema === "object" && schema !== null && FRAME in schema ? ((schema as Record<symbol, unknown>)[FRAME] as z.ZodType) : undefined;
|
|
20
21
|
|
|
21
|
-
// Structural shape of `~orpc
|
|
22
|
-
// oRPC's internal types aren't public.
|
|
22
|
+
// Structural shape of `~orpc`, what oRPC attaches to every procedure: its route, and the RouteMeta its builder carried
|
|
23
|
+
// (route-meta.ts). Read this way since oRPC's internal types aren't public.
|
|
23
24
|
interface ContractProcedureLike {
|
|
24
|
-
readonly "~orpc": { readonly route?: { readonly method?: string; readonly path?: string } };
|
|
25
|
+
readonly "~orpc": { readonly route?: { readonly method?: string; readonly path?: string }; readonly meta?: RouteMeta };
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
const procedureRoute = (value: unknown):
|
|
28
|
+
const procedureRoute = (value: unknown): Omit<ContractRoute, "name"> | undefined => {
|
|
28
29
|
if (typeof value !== "object" || value === null || !("~orpc" in value)) {
|
|
29
30
|
return undefined;
|
|
30
31
|
}
|
|
31
|
-
const { route } = (value as ContractProcedureLike)["~orpc"];
|
|
32
|
+
const { route, meta } = (value as ContractProcedureLike)["~orpc"];
|
|
32
33
|
if (route?.method === undefined || route.path === undefined) {
|
|
33
34
|
return undefined;
|
|
34
35
|
}
|
|
35
|
-
return { method: route.method, path: route.path };
|
|
36
|
+
return { method: route.method, path: route.path, meta: meta ?? {} };
|
|
36
37
|
};
|
|
37
38
|
|
|
38
|
-
// One
|
|
39
|
+
// One declared route: its name and wire shape, matched back to a concrete request by servedRoute, and its policy.
|
|
39
40
|
export interface ContractRoute {
|
|
40
41
|
readonly name: string;
|
|
41
42
|
readonly method: string;
|
|
42
|
-
//
|
|
43
|
+
// Path template with `{param}` placeholders, e.g. `/system/terminals/{name}`; a raw route's may end in `/*`.
|
|
43
44
|
readonly path: string;
|
|
45
|
+
readonly meta: RouteMeta;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
// Walks a contract object (group → procedure) into a flat route list, sorted by name for a stable diff.
|
|
@@ -53,7 +55,7 @@ export const contractRoutes = (contract: Record<string, unknown>): ContractRoute
|
|
|
53
55
|
for (const [name, procedure] of Object.entries(procedures as Record<string, unknown>)) {
|
|
54
56
|
const route = procedureRoute(procedure);
|
|
55
57
|
if (route !== undefined) {
|
|
56
|
-
routes.push({ name: `${group}.${name}`,
|
|
58
|
+
routes.push({ name: `${group}.${name}`, ...route });
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
}
|
|
@@ -143,30 +145,89 @@ export const routeShapes = (contract: Record<string, unknown>): Record<string, s
|
|
|
143
145
|
return shapes;
|
|
144
146
|
};
|
|
145
147
|
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
// A template split once: a literal segment, or undefined for a `{param}`; `tail` for a raw route's trailing `/*`.
|
|
149
|
+
interface Pattern {
|
|
150
|
+
readonly route: ContractRoute;
|
|
151
|
+
readonly segments: readonly (string | undefined)[];
|
|
152
|
+
readonly tail: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const compiled = new WeakMap<readonly ContractRoute[], readonly Pattern[]>();
|
|
156
|
+
|
|
157
|
+
const patternsOf = (routes: readonly ContractRoute[]): readonly Pattern[] => {
|
|
158
|
+
const known = compiled.get(routes);
|
|
159
|
+
if (known !== undefined) {
|
|
160
|
+
return known;
|
|
161
|
+
}
|
|
162
|
+
const patterns = routes.map((route) => {
|
|
163
|
+
const parts = route.path.split("/").slice(1);
|
|
164
|
+
const tail = parts.at(-1) === "*";
|
|
165
|
+
const segments = (tail ? parts.slice(0, -1) : parts).map((part) => (part.startsWith("{") && part.endsWith("}") ? undefined : part));
|
|
166
|
+
return { route, segments, tail };
|
|
167
|
+
});
|
|
168
|
+
compiled.set(routes, patterns);
|
|
169
|
+
return patterns;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Whether a template takes a request's segments. Hono (`raw`) gives a `{param}` only a non-empty segment and a `/*` one
|
|
173
|
+
// or more further ones; oRPC's router gives a `{param}` any segment, the empty one included.
|
|
174
|
+
const takes = (pattern: Pattern, segments: readonly string[], raw: boolean): boolean => {
|
|
175
|
+
const { segments: wanted, tail } = pattern;
|
|
176
|
+
if (tail ? segments.length <= wanted.length : segments.length !== wanted.length) {
|
|
152
177
|
return false;
|
|
153
178
|
}
|
|
154
|
-
return wanted.every((segment, index) => (segment
|
|
179
|
+
return wanted.every((segment, index) => (segment === undefined ? !raw || segments[index] !== "" : segment === segments[index]));
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// oRPC's router tries a literal segment before a parameter, left to right: `/agents/search` is the search route, not
|
|
183
|
+
// `get` with an id of "search", whichever of the two the list happens to hold first.
|
|
184
|
+
const moreLiteral = (a: Pattern, b: Pattern): boolean => {
|
|
185
|
+
const at = a.segments.findIndex((segment, index) => (segment === undefined) !== (b.segments[index] === undefined));
|
|
186
|
+
return at >= 0 && a.segments[at] !== undefined;
|
|
155
187
|
};
|
|
156
188
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
189
|
+
const mostLiteral = (patterns: readonly Pattern[]): Pattern | undefined =>
|
|
190
|
+
patterns.reduce<Pattern | undefined>((best, pattern) => (best === undefined || moreLiteral(pattern, best) ? pattern : best), undefined);
|
|
191
|
+
|
|
192
|
+
// Whether a route declared for one method answers a request's. The CORS middleware answers a preflight (OPTIONS) for
|
|
193
|
+
// every route at its path; Hono (`raw`) answers every method with an `ALL` route and HEAD with a GET one.
|
|
194
|
+
const answers = (declared: string, method: string, raw: boolean): boolean =>
|
|
195
|
+
method === "OPTIONS" || declared === method || (raw && (declared === "ALL" || (declared === "GET" && method === "HEAD")));
|
|
196
|
+
|
|
197
|
+
// A request path as oRPC's router reads it: one trailing slash dropped, then one trailing empty segment, so `/a/` and
|
|
198
|
+
// `/a//` are `/a` while `/a///` is not.
|
|
199
|
+
const orpcSegments = (path: string): string[] => {
|
|
200
|
+
const segments = (path.endsWith("/") ? path.slice(0, -1) : path).split("/").slice(1);
|
|
201
|
+
return segments.at(-1) === "" ? segments.slice(0, -1) : segments;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const contractRouteFor = (routes: readonly ContractRoute[], method: string, path: string): ContractRoute | undefined => {
|
|
205
|
+
const segments = orpcSegments(path);
|
|
206
|
+
return mostLiteral(patternsOf(routes).filter((pattern) => answers(pattern.route.method.toUpperCase(), method, false) && takes(pattern, segments, false)))
|
|
207
|
+
?.route;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Hono matches the path exactly, and among overlapping routes the first registered answers; a preflight belongs to none
|
|
211
|
+
// of them, so the most literal names it.
|
|
212
|
+
const rawRouteFor = (routes: readonly ContractRoute[], method: string, path: string): ContractRoute | undefined => {
|
|
213
|
+
const segments = path.split("/").slice(1);
|
|
214
|
+
const matching = patternsOf(routes).filter((pattern) => answers(pattern.route.method, method, true) && takes(pattern, segments, true));
|
|
215
|
+
return (method === "OPTIONS" ? mostLiteral(matching) : matching[0])?.route;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const pathOf = (pathWithQuery: string): string => pathWithQuery.split("?")[0] ?? pathWithQuery;
|
|
219
|
+
|
|
220
|
+
// The contract route a request belongs to, read as oRPC's router reads it, or undefined for a hand-written daemon route
|
|
221
|
+
// (/health, /workspace/raw) the contract never declared. Query string stripped first.
|
|
222
|
+
export const routeNameForRequest = (routes: readonly ContractRoute[], method: string, pathWithQuery: string): string | undefined =>
|
|
223
|
+
contractRouteFor(routes, method.toUpperCase(), pathOf(pathWithQuery))?.name;
|
|
161
224
|
|
|
162
|
-
|
|
163
|
-
|
|
225
|
+
// The route that serves a request to the daemon: a raw route as Hono matches it, since those are registered ahead of
|
|
226
|
+
// oRPC's catch-all, else a contract route as oRPC's router does; undefined when neither serves it.
|
|
227
|
+
export const servedRoute = (raw: readonly ContractRoute[], contract: readonly ContractRoute[], method: string, pathWithQuery: string): ContractRoute | undefined => {
|
|
164
228
|
const upper = method.toUpperCase();
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return routes
|
|
168
|
-
.filter((route) => route.method.toUpperCase() === upper && pathMatches(route.path, path))
|
|
169
|
-
.toSorted((a, b) => paramCount(a.path) - paramCount(b.path))[0]?.name;
|
|
229
|
+
const path = pathOf(pathWithQuery);
|
|
230
|
+
return rawRouteFor(raw, upper, path) ?? contractRouteFor(contract, upper, path);
|
|
170
231
|
};
|
|
171
232
|
|
|
172
233
|
// The route a typed client call belongs to; oRPC addresses a procedure by contract position (`['git','stashApply']`),
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { AgentTurnSchema, profileOf, TurnProfileSchema } from "./agent.js";
|
|
3
|
+
|
|
4
|
+
// The profile is what every continuation carries whole, so its field set and its normalisation are the contract: a key
|
|
5
|
+
// dropped here is a knob every resume, wake and nudge silently resets.
|
|
6
|
+
describe("TurnProfile", () => {
|
|
7
|
+
test("names exactly the fields that say which turn this is", () => {
|
|
8
|
+
expect(Object.keys(TurnProfileSchema.shape).toSorted()).toEqual([
|
|
9
|
+
"account",
|
|
10
|
+
"actsAs",
|
|
11
|
+
"agent",
|
|
12
|
+
"effort",
|
|
13
|
+
"fast",
|
|
14
|
+
"harness",
|
|
15
|
+
"isolated",
|
|
16
|
+
"model",
|
|
17
|
+
"runRole",
|
|
18
|
+
"thinking",
|
|
19
|
+
"unattended",
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("takes every stated field off a turn, and nothing it says or where it goes", () => {
|
|
24
|
+
const turn = AgentTurnSchema.parse({
|
|
25
|
+
prompt: "Fix the login bug",
|
|
26
|
+
conversationId: "c-1",
|
|
27
|
+
sessionId: "s-1",
|
|
28
|
+
agent: "codex",
|
|
29
|
+
harness: "claude-code",
|
|
30
|
+
account: "acct-2",
|
|
31
|
+
model: "gpt-5",
|
|
32
|
+
effort: "high",
|
|
33
|
+
thinking: false,
|
|
34
|
+
fast: true,
|
|
35
|
+
actsAs: "support",
|
|
36
|
+
isolated: true,
|
|
37
|
+
unattended: true,
|
|
38
|
+
runRole: "loop-iteration",
|
|
39
|
+
permissionMode: "plan",
|
|
40
|
+
autoLand: false,
|
|
41
|
+
});
|
|
42
|
+
expect(profileOf(turn)).toStrictEqual({
|
|
43
|
+
agent: "codex",
|
|
44
|
+
harness: "claude-code",
|
|
45
|
+
account: "acct-2",
|
|
46
|
+
model: "gpt-5",
|
|
47
|
+
effort: "high",
|
|
48
|
+
thinking: false,
|
|
49
|
+
fast: true,
|
|
50
|
+
actsAs: "support",
|
|
51
|
+
isolated: true,
|
|
52
|
+
unattended: true,
|
|
53
|
+
runRole: "loop-iteration",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("leaves absent fields absent rather than naming them undefined", () => {
|
|
58
|
+
expect(profileOf({ model: "sonnet", effort: undefined })).toStrictEqual({ model: "sonnet" });
|
|
59
|
+
expect(Object.keys(profileOf({}))).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// `thinking: false` is a choice the next turn must keep; `isolated: false` only restates the default.
|
|
63
|
+
test("carries a false knob but not a false placement or audience", () => {
|
|
64
|
+
expect(profileOf({ thinking: false, fast: false, isolated: false, unattended: false })).toStrictEqual({ thinking: false, fast: false });
|
|
65
|
+
});
|
|
66
|
+
});
|