@intentic/sandbox-contract 1.337.0 → 1.339.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 +41 -0
- package/dist/chores/probes.d.ts.map +1 -1
- package/dist/chores/probes.js +4 -7
- package/dist/chores/probes.js.map +1 -1
- package/dist/contracts/agent.contract.d.ts +32 -23
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +6020 -5994
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +13 -1
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/approvals.contract.d.ts +8 -0
- package/dist/contracts/approvals.contract.d.ts.map +1 -1
- package/dist/contracts/automations.contract.d.ts +3 -3
- package/dist/contracts/capabilities.contract.d.ts +28 -0
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/capabilities.contract.js +11 -1
- package/dist/contracts/capabilities.contract.js.map +1 -1
- package/dist/contracts/device.contract.d.ts +225 -0
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/device.contract.js +10 -0
- package/dist/contracts/device.contract.js.map +1 -1
- package/dist/contracts/extensions.contract.d.ts +16 -0
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/needs.contract.d.ts +22 -7
- package/dist/contracts/needs.contract.d.ts.map +1 -1
- package/dist/contracts/org.contract.d.ts +35 -0
- package/dist/contracts/org.contract.d.ts.map +1 -0
- package/dist/contracts/org.contract.js +14 -0
- package/dist/contracts/org.contract.js.map +1 -0
- package/dist/contracts/phone.contract.d.ts +1 -0
- package/dist/contracts/phone.contract.d.ts.map +1 -1
- package/dist/contracts/privacy.contract.d.ts +115 -0
- package/dist/contracts/privacy.contract.d.ts.map +1 -1
- package/dist/contracts/privacy.contract.js +11 -2
- package/dist/contracts/privacy.contract.js.map +1 -1
- package/dist/contracts/runner.contract.d.ts +11 -7
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/secrets.contract.d.ts +85 -0
- package/dist/contracts/secrets.contract.d.ts.map +1 -1
- package/dist/contracts/secrets.contract.js +48 -1
- package/dist/contracts/secrets.contract.js.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +4 -1
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +11 -3
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +23 -2
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +17 -4
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/requests.d.ts +4 -0
- package/dist/events/requests.d.ts.map +1 -1
- package/dist/events/requests.js +1 -1
- package/dist/events/requests.js.map +1 -1
- package/dist/events/system-events.d.ts +4 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/system-events.js +2 -0
- package/dist/events/system-events.js.map +1 -1
- package/dist/events/transcript.d.ts +27 -6
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/index.d.ts +7562 -7426
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -257
- package/dist/index.js.map +1 -1
- package/dist/models/agent-lanes.d.ts +30 -0
- package/dist/models/agent-lanes.d.ts.map +1 -0
- package/dist/models/agent-lanes.js +67 -0
- package/dist/models/agent-lanes.js.map +1 -0
- package/dist/models/agent-standing.d.ts +55 -0
- package/dist/models/agent-standing.d.ts.map +1 -0
- package/dist/models/agent-standing.js +82 -0
- package/dist/models/agent-standing.js.map +1 -0
- package/dist/models/agent-starter.d.ts +10 -0
- package/dist/models/agent-starter.d.ts.map +1 -0
- package/dist/models/agent-starter.js +16 -0
- package/dist/models/agent-starter.js.map +1 -0
- package/dist/netd/generated/wire.d.ts +1 -1
- package/dist/netd/generated/wire.d.ts.map +1 -1
- package/dist/policy/identity-assertion.d.ts.map +1 -1
- package/dist/policy/identity-assertion.js +2 -8
- package/dist/policy/identity-assertion.js.map +1 -1
- package/dist/policy/keyring-seal.d.ts +48 -0
- package/dist/policy/keyring-seal.d.ts.map +1 -0
- package/dist/policy/keyring-seal.js +94 -0
- package/dist/policy/keyring-seal.js.map +1 -0
- package/dist/policy/message-limits.d.ts +2 -0
- package/dist/policy/message-limits.d.ts.map +1 -1
- package/dist/policy/message-limits.js +2 -0
- package/dist/policy/message-limits.js.map +1 -1
- package/dist/policy/org-locks.d.ts +58 -0
- package/dist/policy/org-locks.d.ts.map +1 -0
- package/dist/policy/org-locks.js +113 -0
- package/dist/policy/org-locks.js.map +1 -0
- package/dist/policy/persona-capability.d.ts +14 -0
- package/dist/policy/persona-capability.d.ts.map +1 -0
- package/dist/policy/persona-capability.js +23 -0
- package/dist/policy/persona-capability.js.map +1 -0
- package/dist/policy/reserved-servers.d.ts.map +1 -1
- package/dist/policy/reserved-servers.js +1 -1
- package/dist/policy/reserved-servers.js.map +1 -1
- package/dist/protocol/doors/phone-links.d.ts +5 -0
- package/dist/protocol/doors/phone-links.d.ts.map +1 -1
- package/dist/protocol/doors/phone-links.js +3 -1
- package/dist/protocol/doors/phone-links.js.map +1 -1
- package/dist/protocol/doors/webext-links.d.ts +3 -0
- package/dist/protocol/doors/webext-links.d.ts.map +1 -1
- package/dist/protocol/doors/webext-links.js +3 -0
- package/dist/protocol/doors/webext-links.js.map +1 -1
- package/dist/protocol/raw/raw-json-routes.d.ts +270 -0
- package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-json-routes.js +14 -1
- package/dist/protocol/raw/raw-json-routes.js.map +1 -1
- package/dist/protocol/raw/raw-routes.d.ts +70 -0
- package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-routes.js +18 -0
- package/dist/protocol/raw/raw-routes.js.map +1 -1
- package/dist/protocol/route-meta.d.ts.map +1 -1
- package/dist/protocol/route-meta.js.map +1 -1
- package/dist/protocol/routes.d.ts +4 -0
- package/dist/protocol/routes.d.ts.map +1 -1
- package/dist/protocol/routes.js +12 -0
- package/dist/protocol/routes.js.map +1 -1
- package/dist/schemas/agents-list.d.ts +5 -0
- package/dist/schemas/agents-list.d.ts.map +1 -1
- package/dist/schemas/agents-list.js +8 -0
- package/dist/schemas/agents-list.js.map +1 -1
- package/dist/schemas/agents.d.ts +1 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +1 -0
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/approvals.d.ts +20 -0
- package/dist/schemas/approvals.d.ts.map +1 -1
- package/dist/schemas/approvals.js +10 -0
- package/dist/schemas/approvals.js.map +1 -1
- package/dist/schemas/capabilities.d.ts +26 -0
- package/dist/schemas/capabilities.d.ts.map +1 -1
- package/dist/schemas/capabilities.js +17 -0
- package/dist/schemas/capabilities.js.map +1 -1
- package/dist/schemas/data-protection.d.ts +5 -0
- package/dist/schemas/data-protection.d.ts.map +1 -1
- package/dist/schemas/data-protection.js +29 -0
- package/dist/schemas/data-protection.js.map +1 -1
- package/dist/schemas/devices.d.ts +20 -0
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +5 -1
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/extension-updates.d.ts +32 -0
- package/dist/schemas/extension-updates.d.ts.map +1 -1
- package/dist/schemas/git/git.d.ts +19 -0
- package/dist/schemas/git/git.d.ts.map +1 -1
- package/dist/schemas/git/git.js +12 -1
- package/dist/schemas/git/git.js.map +1 -1
- package/dist/schemas/hosts.d.ts +2 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +11 -1
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/intentic-os.d.ts +218 -0
- package/dist/schemas/intentic-os.d.ts.map +1 -1
- package/dist/schemas/intentic-os.js +166 -0
- package/dist/schemas/intentic-os.js.map +1 -1
- package/dist/schemas/keyring.d.ts +421 -0
- package/dist/schemas/keyring.d.ts.map +1 -0
- package/dist/schemas/keyring.js +176 -0
- package/dist/schemas/keyring.js.map +1 -0
- package/dist/schemas/links.d.ts +208 -0
- package/dist/schemas/links.d.ts.map +1 -0
- package/dist/schemas/links.js +116 -0
- package/dist/schemas/links.js.map +1 -0
- package/dist/schemas/maintenance.d.ts.map +1 -1
- package/dist/schemas/maintenance.js.map +1 -1
- package/dist/schemas/members.d.ts.map +1 -1
- package/dist/schemas/members.js +2 -0
- package/dist/schemas/members.js.map +1 -1
- package/dist/schemas/needs.d.ts +25 -8
- package/dist/schemas/needs.d.ts.map +1 -1
- package/dist/schemas/needs.js +15 -3
- package/dist/schemas/needs.js.map +1 -1
- package/dist/schemas/organisation.d.ts +106 -0
- package/dist/schemas/organisation.d.ts.map +1 -0
- package/dist/schemas/organisation.js +52 -0
- package/dist/schemas/organisation.js.map +1 -0
- package/dist/schemas/phone.d.ts +3 -0
- package/dist/schemas/phone.d.ts.map +1 -1
- package/dist/schemas/phone.js +1 -0
- package/dist/schemas/phone.js.map +1 -1
- package/dist/schemas/privacy.d.ts +232 -0
- package/dist/schemas/privacy.d.ts.map +1 -1
- package/dist/schemas/privacy.js +154 -4
- package/dist/schemas/privacy.js.map +1 -1
- package/dist/schemas/secrets.d.ts +151 -0
- package/dist/schemas/secrets.d.ts.map +1 -1
- package/dist/schemas/secrets.js +71 -8
- package/dist/schemas/secrets.js.map +1 -1
- package/dist/schemas/settings.d.ts +8 -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/schemas/speech.d.ts +12 -0
- package/dist/schemas/speech.d.ts.map +1 -1
- package/dist/schemas/speech.js +23 -0
- package/dist/schemas/speech.js.map +1 -1
- package/dist/schemas/system.d.ts +2 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +1 -1
- package/dist/schemas/system.js.map +1 -1
- package/dist/schemas/terminal.d.ts +15 -0
- package/dist/schemas/terminal.d.ts.map +1 -1
- package/dist/schemas/terminal.js +11 -2
- package/dist/schemas/terminal.js.map +1 -1
- package/dist/schemas/voice.d.ts +3 -0
- package/dist/schemas/voice.d.ts.map +1 -1
- package/dist/schemas/voice.js +7 -0
- package/dist/schemas/voice.js.map +1 -1
- package/dist/schemas/webext.d.ts +17 -0
- package/dist/schemas/webext.d.ts.map +1 -1
- package/dist/schemas/webext.js +7 -1
- package/dist/schemas/webext.js.map +1 -1
- package/dist/state/contract-lock.d.ts +3 -1
- package/dist/state/contract-lock.d.ts.map +1 -1
- package/dist/state/contract-lock.js +34 -9
- package/dist/state/contract-lock.js.map +1 -1
- package/dist/state/contract-modules.d.ts +4 -0
- package/dist/state/contract-modules.d.ts.map +1 -0
- package/dist/state/contract-modules.js +28 -0
- package/dist/state/contract-modules.js.map +1 -0
- package/dist/state/daemon-floor.d.ts +4 -0
- package/dist/state/daemon-floor.d.ts.map +1 -0
- package/dist/state/daemon-floor.js +8 -0
- package/dist/state/daemon-floor.js.map +1 -0
- package/dist/state/definition.d.ts +34 -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 +8 -0
- package/dist/state/history-state.js.map +1 -1
- package/dist/text/documents.d.ts +3 -1
- package/dist/text/documents.d.ts.map +1 -1
- package/dist/text/documents.js +30 -0
- package/dist/text/documents.js.map +1 -1
- package/dist/text/pages.d.ts.map +1 -1
- package/dist/text/pages.js +3 -7
- package/dist/text/pages.js.map +1 -1
- package/package.json +27 -5
- package/src/chores/probes.ts +4 -6
- package/src/contracts/agents.contract.ts +18 -1
- package/src/contracts/capabilities.contract.ts +14 -0
- package/src/contracts/device.contract.ts +30 -0
- package/src/contracts/org.contract.ts +19 -0
- package/src/contracts/privacy.contract.ts +13 -1
- package/src/contracts/secrets.contract.ts +64 -0
- package/src/events/requests.ts +3 -1
- package/src/events/system-events.ts +9 -0
- package/src/index.ts +41 -279
- package/src/models/agent-lanes.ts +152 -0
- package/src/models/agent-standing.ts +279 -0
- package/src/models/agent-starter.ts +26 -0
- package/src/netd/generated/netd-wire.json +2 -1
- package/src/netd/generated/wire.ts +1 -1
- package/src/policy/identity-assertion.ts +2 -8
- package/src/policy/keyring-seal.test.ts +93 -0
- package/src/policy/keyring-seal.ts +148 -0
- package/src/policy/message-limits.ts +3 -0
- package/src/policy/org-locks.ts +173 -0
- package/src/policy/persona-capability.ts +57 -0
- package/src/policy/reserved-servers.ts +3 -2
- package/src/policy/tests/org-locks.test.ts +92 -0
- package/src/policy/tests/reserved-servers.test.ts +3 -2
- package/src/protocol/doors/listener-protocol.ts +1 -1
- package/src/protocol/doors/phone-links.ts +11 -2
- package/src/protocol/doors/phone-protocol.test.ts +8 -1
- package/src/protocol/doors/webext-links.ts +13 -0
- package/src/protocol/ingress-contract.fixture.json +2 -0
- package/src/protocol/pairing-redeem.ts +1 -1
- package/src/protocol/peer-dial.test.ts +6 -6
- package/src/protocol/peer-mcp-server.test.ts +2 -1
- package/src/protocol/raw/raw-json-routes.ts +31 -1
- package/src/protocol/raw/raw-routes.ts +35 -1
- package/src/protocol/route-meta.ts +2 -1
- package/src/protocol/routes.test.ts +33 -1
- package/src/protocol/routes.ts +18 -0
- package/src/protocol/sse.ts +1 -1
- package/src/schemas/agent.ts +1 -1
- package/src/schemas/agents-list.ts +13 -0
- package/src/schemas/agents.ts +1 -0
- package/src/schemas/approvals.ts +14 -0
- package/src/schemas/capabilities.ts +25 -0
- package/src/schemas/data-protection.ts +47 -0
- package/src/schemas/devices.ts +15 -1
- package/src/schemas/git/git.ts +22 -2
- package/src/schemas/hosts.ts +14 -1
- package/src/schemas/intentic-os.test.ts +148 -0
- package/src/schemas/intentic-os.ts +259 -0
- package/src/schemas/keyring.ts +277 -0
- package/src/schemas/links.ts +180 -0
- package/src/schemas/maintenance.ts +1 -0
- package/src/schemas/members.ts +4 -0
- package/src/schemas/needs.ts +21 -5
- package/src/schemas/organisation.ts +110 -0
- package/src/schemas/phone.ts +3 -0
- package/src/schemas/privacy.test.ts +63 -0
- package/src/schemas/privacy.ts +213 -5
- package/src/schemas/secrets.ts +96 -8
- package/src/schemas/settings.ts +8 -0
- package/src/schemas/speech.ts +38 -1
- package/src/schemas/system.ts +5 -2
- package/src/schemas/terminal.ts +13 -2
- package/src/schemas/voice.ts +11 -0
- package/src/schemas/webext.ts +16 -1
- package/src/state/contract-lock.test.ts +12 -12
- package/src/state/contract-lock.ts +43 -12
- package/src/state/contract-modules.ts +41 -0
- package/src/state/daemon-floor.test.ts +81 -0
- package/src/state/daemon-floor.ts +32 -0
- package/src/state/history-state.ts +17 -0
- package/src/state/workspace-state.ts +1 -1
- package/src/text/documents.test.ts +30 -1
- package/src/text/documents.ts +43 -1
- package/src/text/pages.ts +5 -7
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { ORG_SECTIONS, type OrgPolicy, type OrgSection, orgSectionMode } from "../schemas/organisation.js";
|
|
2
|
+
|
|
3
|
+
// WHICH DAEMON ROUTES AN ORGANISATION'S LOCK COVERS, and the refusal the daemon's middleware hands a request to one of
|
|
4
|
+
// them (docs/architecture/organisations.md). Named route by route rather than by contract group: a group mixes the
|
|
5
|
+
// changes a lock is about with operations it is not (a probe, a reveal, a sign-in being cancelled, a person confirming
|
|
6
|
+
// their own AI-literacy notice), and a lock that broke those would be a lock nobody could live with. A route added
|
|
7
|
+
// later is therefore open until it is listed here, which is the failure a reviewer can see in this file's diff.
|
|
8
|
+
//
|
|
9
|
+
// Names are the contract's own: `group.procedure` for a typed route, `METHOD /path` for a raw one (RAW_ROUTES). The
|
|
10
|
+
// test beside this checks every name against the route tables, so a rename cannot leave a lock pointing at nothing.
|
|
11
|
+
|
|
12
|
+
interface SectionRoutes {
|
|
13
|
+
// Refused while the section is locked or hidden: every change the section's page makes.
|
|
14
|
+
readonly changes: readonly string[];
|
|
15
|
+
// Refused only while it is hidden: reads that put a value in front of a person, which hiding is meant to stop.
|
|
16
|
+
readonly reveals?: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const ORG_SECTION_ROUTES = {
|
|
20
|
+
models: { changes: ["accounts.start", "accounts.complete", "accounts.rename", "accounts.disconnect"] },
|
|
21
|
+
agent: {
|
|
22
|
+
// `privacy.learn` stays open: teaching the shield more values only ever adds protection.
|
|
23
|
+
changes: [
|
|
24
|
+
"settings.set",
|
|
25
|
+
"settings.setAgentDomain",
|
|
26
|
+
"privacy.setPolicy",
|
|
27
|
+
"privacy.forget",
|
|
28
|
+
"safety.setPolicy",
|
|
29
|
+
"skills.save",
|
|
30
|
+
"skills.remove",
|
|
31
|
+
],
|
|
32
|
+
reveals: ["privacy.reveal"],
|
|
33
|
+
},
|
|
34
|
+
// Rejecting a proposed tool and timing a rebuild stay open: neither changes what the sandbox is made of.
|
|
35
|
+
environment: {
|
|
36
|
+
changes: [
|
|
37
|
+
"POST /environment/approve",
|
|
38
|
+
"POST /environment/runtime-install",
|
|
39
|
+
"POST /environment/remove",
|
|
40
|
+
"POST /engines/channel",
|
|
41
|
+
"POST /engines/update",
|
|
42
|
+
"POST /engines/revert",
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
// Secrets and connections share the Secrets page. Asking a named person to release one (`secrets.request`) stays
|
|
46
|
+
// open: it changes nothing until that person says yes.
|
|
47
|
+
secrets: {
|
|
48
|
+
changes: [
|
|
49
|
+
"secrets.set",
|
|
50
|
+
"secrets.generate",
|
|
51
|
+
"secrets.remove",
|
|
52
|
+
"secrets.setGate",
|
|
53
|
+
"secrets.removeGate",
|
|
54
|
+
"secrets.setHosts",
|
|
55
|
+
"secrets.setPolicy",
|
|
56
|
+
"capabilities.add",
|
|
57
|
+
"capabilities.remove",
|
|
58
|
+
"capabilities.rename",
|
|
59
|
+
"capabilities.setSecret",
|
|
60
|
+
"capabilities.sshKey",
|
|
61
|
+
"capabilities.login",
|
|
62
|
+
],
|
|
63
|
+
reveals: ["secrets.reveal"],
|
|
64
|
+
},
|
|
65
|
+
"data-protection": { changes: ["compliance.applyPreset"] },
|
|
66
|
+
extensions: {
|
|
67
|
+
changes: [
|
|
68
|
+
"extensions.create",
|
|
69
|
+
"extensions.remove",
|
|
70
|
+
"extensions.setSettings",
|
|
71
|
+
"extensions.approve",
|
|
72
|
+
"extensions.setEnabled",
|
|
73
|
+
"extensions.applyUpdate",
|
|
74
|
+
"extensions.revert",
|
|
75
|
+
"extensions.setUpdatePolicy",
|
|
76
|
+
"extensions.devSet",
|
|
77
|
+
"extensions.devClear",
|
|
78
|
+
"extensions.devReload",
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
// Leaving (`DELETE /members/self`) stays open: nobody can be held in a sandbox by a policy.
|
|
82
|
+
access: { changes: ["POST /members", "DELETE /members"] },
|
|
83
|
+
areas: { changes: ["areas.save", "areas.remove"] },
|
|
84
|
+
personas: { changes: ["personas.save", "personas.remove", "personas.savePrompt", "personas.saveSkill", "personas.removeSkill"] },
|
|
85
|
+
updates: { changes: ["system.autoUpdate", "system.skipUpdate"] },
|
|
86
|
+
} as const satisfies Readonly<Record<OrgSection, SectionRoutes>>;
|
|
87
|
+
|
|
88
|
+
// Refused on ANY bound sandbox, whatever its sections say: acts by which the sandbox's owner would take the organisation's
|
|
89
|
+
// sandbox away from everyone else in it. Closing the daemon to every browser (what deleting one's account does to each
|
|
90
|
+
// owned sandbox) would leave a department's work unreachable by the colleagues who carry it on; the organisation
|
|
91
|
+
// releases the sandbox first if that is what it wants. Listed under `access`, the part of the sandbox it belongs to.
|
|
92
|
+
export const ORG_BOUND_ROUTES: readonly string[] = ["POST /system/access/disable"];
|
|
93
|
+
|
|
94
|
+
// Route name → the section it belongs to and whether it is a change or a reveal; built once.
|
|
95
|
+
type Covered = { readonly section: OrgSection; readonly kind: "change" | "reveal" | "bound" };
|
|
96
|
+
const COVERED: ReadonlyMap<string, Covered> = (() => {
|
|
97
|
+
const covered = new Map<string, Covered>();
|
|
98
|
+
for (const section of ORG_SECTIONS) {
|
|
99
|
+
const routes: SectionRoutes = ORG_SECTION_ROUTES[section];
|
|
100
|
+
for (const name of routes.changes) {
|
|
101
|
+
covered.set(name, { section, kind: "change" });
|
|
102
|
+
}
|
|
103
|
+
for (const name of routes.reveals ?? []) {
|
|
104
|
+
covered.set(name, { section, kind: "reveal" });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const name of ORG_BOUND_ROUTES) {
|
|
108
|
+
covered.set(name, { section: "access", kind: "bound" });
|
|
109
|
+
}
|
|
110
|
+
return covered;
|
|
111
|
+
})();
|
|
112
|
+
|
|
113
|
+
// The section a route belongs to, or undefined for one no lock covers.
|
|
114
|
+
export const orgSectionOfRoute = (routeName: string): OrgSection | undefined => COVERED.get(routeName)?.section;
|
|
115
|
+
|
|
116
|
+
export interface OrgLockRefusal {
|
|
117
|
+
readonly error: string;
|
|
118
|
+
readonly reason: "org-locked";
|
|
119
|
+
readonly section: OrgSection;
|
|
120
|
+
readonly org: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The refusal for one request, or undefined to let it through. No policy (a personal sandbox) is always undefined:
|
|
124
|
+
// this function is the whole of what an organisation changes about a request, and for a personal sandbox it is nothing.
|
|
125
|
+
export const orgLockRefusal = (policy: OrgPolicy | undefined, routeName: string | undefined): OrgLockRefusal | undefined => {
|
|
126
|
+
if (policy === undefined || routeName === undefined) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const covered = COVERED.get(routeName);
|
|
130
|
+
if (covered === undefined) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
const mode = orgSectionMode(policy, covered.section);
|
|
134
|
+
const refused = covered.kind === "bound" || (covered.kind === "change" ? mode !== "open" : mode === "hidden");
|
|
135
|
+
return refused
|
|
136
|
+
? {
|
|
137
|
+
error: `${policy.orgName} manages this part of the sandbox (${covered.section}); ask your organisation's admin to change it`,
|
|
138
|
+
reason: "org-locked",
|
|
139
|
+
section: covered.section,
|
|
140
|
+
org: policy.orgName,
|
|
141
|
+
}
|
|
142
|
+
: undefined;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// A write refused because it would undo an organisation's pinned settings (schemas/organisation.ts `preset`): the
|
|
146
|
+
// preset items it would have loosened, by the ids the data protection page shows them under.
|
|
147
|
+
export interface OrgPinRefusal {
|
|
148
|
+
readonly error: string;
|
|
149
|
+
readonly reason: "org-pinned";
|
|
150
|
+
readonly items: readonly string[];
|
|
151
|
+
readonly org: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const orgPinRefusal = (policy: Pick<OrgPolicy, "orgName">, items: readonly string[]): OrgPinRefusal => ({
|
|
155
|
+
error: `${policy.orgName} requires ${items.join(", ")} to stay as it set them; ask your organisation's admin to change it`,
|
|
156
|
+
reason: "org-pinned",
|
|
157
|
+
items,
|
|
158
|
+
org: policy.orgName,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// A grant refused because the organisation keeps its sandboxes to its own members (schemas/organisation.ts
|
|
162
|
+
// `membersOnly`).
|
|
163
|
+
export interface OrgMembersOnlyRefusal {
|
|
164
|
+
readonly error: string;
|
|
165
|
+
readonly reason: "org-members-only";
|
|
166
|
+
readonly org: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const orgMembersOnlyRefusal = (policy: Pick<OrgPolicy, "orgName">): OrgMembersOnlyRefusal => ({
|
|
170
|
+
error: `${policy.orgName} gives access to its sandboxes only to its own members; ask your organisation's admin to add this person first`,
|
|
171
|
+
reason: "org-members-only",
|
|
172
|
+
org: policy.orgName,
|
|
173
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// WHETHER A PERSONA LETS A TURN ACT THROUGH ONE CONNECTED CAPABILITY. Pure and shared, because the daemon mounts a turn's
|
|
2
|
+
// tools on this answer (personas/personas.ts `turnPersona`) and the Sandbox ▸ Map view draws who reaches what from it,
|
|
3
|
+
// and two implementations would disagree at exactly the boundary that matters.
|
|
4
|
+
|
|
5
|
+
// The two fields of a connection the rule reads; structural, so neither side has to hand over its own record type.
|
|
6
|
+
export interface CapabilityRef {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly kind: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// The persona's shelves the rule reads, fully resolved (PersonaPowersSchema's defaults applied): a list absent means
|
|
12
|
+
// every connection of its kind, an empty one none.
|
|
13
|
+
export interface PersonaShelves {
|
|
14
|
+
readonly sandbox: boolean;
|
|
15
|
+
readonly delegate: boolean;
|
|
16
|
+
readonly connectors?: readonly string[] | undefined;
|
|
17
|
+
readonly devices?: readonly string[] | undefined;
|
|
18
|
+
readonly mcp?: readonly string[] | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The kinds that are someone signed in: a site account, and the identity whose browser holds every account born of it. */
|
|
22
|
+
export const isSignedInAccount = (kind: string): boolean => kind === "browser" || kind === "identity";
|
|
23
|
+
|
|
24
|
+
const listed = (list: readonly string[] | undefined, id: string): boolean => list === undefined || list.includes(id);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether a persona carrying these accounts (its `capabilities`) and these shelves may act through a capability. A kind
|
|
28
|
+
* absent from the switch (an agent runtime, a devops entry, a VPN) passes untouched, so a new kind is never silently
|
|
29
|
+
* denied.
|
|
30
|
+
*/
|
|
31
|
+
export const personaAllowsCapability = (capability: CapabilityRef, accounts: readonly string[], shelves: PersonaShelves): boolean => {
|
|
32
|
+
switch (capability.kind) {
|
|
33
|
+
// The signed-in browsers: the persona's own `capabilities` list, and the field that predates the shelves. Granting
|
|
34
|
+
// an account (`reddit-work`) grants its hands; granting the identity itself grants the someone.
|
|
35
|
+
case "browser":
|
|
36
|
+
case "identity":
|
|
37
|
+
return accounts.includes(capability.id);
|
|
38
|
+
// Connector credentials reach the shell; an ungranted id keeps its env vars out of the turn entirely.
|
|
39
|
+
case "cli":
|
|
40
|
+
return listed(shelves.connectors, capability.id);
|
|
41
|
+
// A phone is one of the owner's own devices, on the same shelf as their computers.
|
|
42
|
+
case "device":
|
|
43
|
+
case "phone":
|
|
44
|
+
return listed(shelves.devices, capability.id);
|
|
45
|
+
case "mcp":
|
|
46
|
+
return listed(shelves.mcp, capability.id);
|
|
47
|
+
// Making sandboxes on the owner's account is administering what this one runs beside: the sandbox shelf.
|
|
48
|
+
case "fleet":
|
|
49
|
+
return shelves.sandbox;
|
|
50
|
+
// Another of the owner's sandboxes sits on the devices list beside their computers, picked one by one, and handing
|
|
51
|
+
// it a task is delegating, as spawning a subagent is.
|
|
52
|
+
case "link":
|
|
53
|
+
return shelves.delegate && listed(shelves.devices, capability.id);
|
|
54
|
+
default:
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -41,11 +41,12 @@ export const CONTROL_MCP_SERVERS: ReadonlySet<string> = new Set(
|
|
|
41
41
|
);
|
|
42
42
|
|
|
43
43
|
// Capability kinds whose id becomes an MCP server name for the turn, and so whose id must not collide with a daemon
|
|
44
|
-
// server: `mcp`, a core kind that is its own endpoint,
|
|
44
|
+
// server: `mcp`, a core kind that is its own endpoint, `link`, whose card is mounted as a server named after it, and
|
|
45
|
+
// every contributable kind the manifest schema says mints one
|
|
45
46
|
// (a device's and a connected browser's peer bridge, a cli card that serves tools), read off the schema's own meaning
|
|
46
47
|
// (`contributedServerMintingKinds`), so a kind that starts minting is refused here without a second list. Other kinds
|
|
47
48
|
// mint accounts, providers or infrastructure, never a `mcp__<id>__` server keyed by the id.
|
|
48
|
-
export const MCP_SERVER_MINTING_KINDS: ReadonlySet<string> = new Set(["mcp", ...contributedServerMintingKinds()]);
|
|
49
|
+
export const MCP_SERVER_MINTING_KINDS: ReadonlySet<string> = new Set(["mcp", "link", ...contributedServerMintingKinds()]);
|
|
49
50
|
|
|
50
51
|
// Whether adding or renaming a capability of this kind to this id would shadow a daemon server. The one predicate the
|
|
51
52
|
// capability routes consult, so the refusal and this list can't drift.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { RAW_ROUTE_LIST } from "../../protocol/raw/raw-routes.js";
|
|
2
|
+
import { SANDBOX_ROUTES, sandboxRouteFor } from "../../index.js";
|
|
3
|
+
import { type OrgPolicy, OrgPolicySchema, orgPinnedGroups, orgUnknownPins } from "../../schemas/organisation.js";
|
|
4
|
+
import { ORG_BOUND_ROUTES, ORG_SECTION_ROUTES, orgLockRefusal, orgPinRefusal, orgSectionOfRoute } from "../org-locks.js";
|
|
5
|
+
|
|
6
|
+
const policy = (sections: OrgPolicy["sections"]): OrgPolicy => ({ orgId: "org-1", orgName: "Urząd Gminy", version: 3, sections });
|
|
7
|
+
|
|
8
|
+
describe("org locks", () => {
|
|
9
|
+
it("names only routes that exist, and never a read as a change", () => {
|
|
10
|
+
const known = new Map([...SANDBOX_ROUTES, ...RAW_ROUTE_LIST].map((route) => [route.name, route.method]));
|
|
11
|
+
for (const [section, routes] of Object.entries(ORG_SECTION_ROUTES)) {
|
|
12
|
+
for (const name of routes.changes) {
|
|
13
|
+
expect({ section, name, method: known.get(name) }).toEqual({ section, name, method: expect.not.stringMatching(/^(GET|HEAD)$/) });
|
|
14
|
+
}
|
|
15
|
+
for (const name of "reveals" in routes ? routes.reveals : []) {
|
|
16
|
+
expect({ section, name, known: known.has(name) }).toEqual({ section, name, known: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("lets everything through for a personal sandbox", () => {
|
|
22
|
+
expect(orgLockRefusal(undefined, "settings.set")).toBeUndefined();
|
|
23
|
+
expect(orgLockRefusal(undefined, "secrets.reveal")).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("refuses a change to a locked section and names the organisation", () => {
|
|
27
|
+
const refused = orgLockRefusal(policy({ agent: "locked" }), "settings.set");
|
|
28
|
+
expect(refused).toMatchObject({ reason: "org-locked", section: "agent", org: "Urząd Gminy" });
|
|
29
|
+
expect(orgLockRefusal(policy({ agent: "locked" }), "secrets.set")).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("keeps reveals open while locked and refuses them once hidden", () => {
|
|
33
|
+
expect(orgLockRefusal(policy({ secrets: "locked" }), "secrets.reveal")).toBeUndefined();
|
|
34
|
+
expect(orgLockRefusal(policy({ secrets: "locked" }), "secrets.set")).toMatchObject({ section: "secrets" });
|
|
35
|
+
expect(orgLockRefusal(policy({ secrets: "hidden" }), "secrets.reveal")).toMatchObject({ section: "secrets" });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("leaves a person's own exits open whatever the policy", () => {
|
|
39
|
+
const everything = policy({ access: "hidden", agent: "hidden" });
|
|
40
|
+
expect(orgLockRefusal(everything, "DELETE /members/self")).toBeUndefined();
|
|
41
|
+
expect(orgLockRefusal(everything, "settings.confirmAiLiteracy")).toBeUndefined();
|
|
42
|
+
expect(orgLockRefusal(everything, "secrets.request")).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("resolves a concrete request to its section through the route table", () => {
|
|
46
|
+
expect(orgSectionOfRoute(sandboxRouteFor("POST", "/members")?.name ?? "")).toBe("access");
|
|
47
|
+
expect(orgSectionOfRoute(sandboxRouteFor("DELETE", "/members/self")?.name ?? "")).toBeUndefined();
|
|
48
|
+
expect(orgSectionOfRoute(sandboxRouteFor("PUT", "/secrets/gates/OPENAI_KEY")?.name ?? "")).toBe("secrets");
|
|
49
|
+
expect(orgSectionOfRoute(sandboxRouteFor("POST", "/system/update/auto")?.name ?? "")).toBe("updates");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("names only bound-only routes that exist and change something", () => {
|
|
53
|
+
const known = new Map([...SANDBOX_ROUTES, ...RAW_ROUTE_LIST].map((route) => [route.name, route.method]));
|
|
54
|
+
for (const name of ORG_BOUND_ROUTES) {
|
|
55
|
+
expect({ name, method: known.get(name) }).toEqual({ name, method: expect.not.stringMatching(/^(GET|HEAD)$/) });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("refuses closing the sandbox to every browser on any bound sandbox, an all-open policy included", () => {
|
|
60
|
+
expect(orgLockRefusal(policy({}), "POST /system/access/disable")).toMatchObject({ reason: "org-locked", section: "access", org: "Urząd Gminy" });
|
|
61
|
+
expect(orgLockRefusal(policy({ access: "open" }), "POST /system/access/disable")).toMatchObject({ section: "access" });
|
|
62
|
+
expect(orgLockRefusal(undefined, "POST /system/access/disable")).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("reads pinned preset groups it knows in the contract's order, and reports the ones it does not", () => {
|
|
66
|
+
const pinned = OrgPolicySchema.parse({ orgId: "o", orgName: "O", version: 1, preset: ["judge", "telepathy", "privacy"] });
|
|
67
|
+
expect(orgPinnedGroups(pinned)).toEqual(["privacy", "judge"]);
|
|
68
|
+
expect(orgUnknownPins(pinned)).toEqual(["telepathy"]);
|
|
69
|
+
expect(orgPinnedGroups(OrgPolicySchema.parse({ orgId: "o", orgName: "O", version: 1 }))).toEqual([]);
|
|
70
|
+
expect(orgPinnedGroups(undefined)).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("names the organisation and the items a refused pinned write would have loosened", () => {
|
|
74
|
+
expect(orgPinRefusal({ orgName: "Urząd Gminy" }, ["privacy.mode", "retention.transcriptDays"])).toEqual({
|
|
75
|
+
error: "Urząd Gminy requires privacy.mode, retention.transcriptDays to stay as it set them; ask your organisation's admin to change it",
|
|
76
|
+
reason: "org-pinned",
|
|
77
|
+
items: ["privacy.mode", "retention.transcriptDays"],
|
|
78
|
+
org: "Urząd Gminy",
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("takes the people list only as sha256 digests", () => {
|
|
83
|
+
const base = { orgId: "o", orgName: "O", version: 1, membersOnly: true };
|
|
84
|
+
expect(OrgPolicySchema.safeParse({ ...base, people: ["a".repeat(64)] }).success).toBe(true);
|
|
85
|
+
expect(OrgPolicySchema.safeParse({ ...base, people: ["clerk@gmina.test"] }).success).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("reads a section a policy leaves out as open, and refuses an unknown mode", () => {
|
|
89
|
+
expect(OrgPolicySchema.parse({ orgId: "o", orgName: "O", version: 1 }).sections).toEqual({});
|
|
90
|
+
expect(OrgPolicySchema.safeParse({ orgId: "o", orgName: "O", version: 1, sections: { agent: "sealed" } }).success).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -25,9 +25,10 @@ describe("reserved MCP server names", () => {
|
|
|
25
25
|
|
|
26
26
|
describe("collidesWithReservedServer", () => {
|
|
27
27
|
// Derived from the manifest schema's meanings: a cli card mints a server when it serves tools, a device, a
|
|
28
|
-
// connected browser and a connected phone always do,
|
|
28
|
+
// connected browser and a connected phone always do, `mcp` is the core kind that is its own endpoint, and a `link`
|
|
29
|
+
// card is mounted as a server named after it.
|
|
29
30
|
test("the minting kinds are exactly the ones whose id becomes a server name", () => {
|
|
30
|
-
expect([...MCP_SERVER_MINTING_KINDS].toSorted()).toEqual(["cli", "device", "mcp", "phone", "webext"]);
|
|
31
|
+
expect([...MCP_SERVER_MINTING_KINDS].toSorted()).toEqual(["cli", "device", "link", "mcp", "phone", "webext"]);
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
test("a kind whose id becomes a server name may not reuse a reserved one", () => {
|
|
@@ -4,7 +4,7 @@ import { ActivityStatusSchema } from "../../schemas/activity.js";
|
|
|
4
4
|
import { rawRouteUrl } from "../raw/raw-routes.js";
|
|
5
5
|
|
|
6
6
|
// Wire between the daemon and an extension's realtime-listener gateway (ext-discord, ext-slack, ext-telegram,
|
|
7
|
-
// ext-whatsapp, ext-imap, ext-google-workspace): the five /listeners/:provider routes (state, dispatch, failure,
|
|
7
|
+
// ext-whatsapp, ext-imap, ext-google-workspace, ext-microsoft-365): the five /listeners/:provider routes (state, dispatch, failure,
|
|
8
8
|
// status, stop). Shared here so both ends compile against one declaration.
|
|
9
9
|
|
|
10
10
|
// The path of one of the five listener routes for a provider, built from RAW_ROUTES so the gateway and the daemon
|
|
@@ -18,6 +18,10 @@ const PAIRING_PREFIX = "ixp1_";
|
|
|
18
18
|
// that offers it. The code rides the fragment, which no server ever receives, so no access log holds a live pairing.
|
|
19
19
|
export const PHONE_PAIR_LINK = "https://intentic.dev/phone/pair";
|
|
20
20
|
|
|
21
|
+
// A watch has no camera to scan with, so its code is scanned by the phone beside it: this link opens Intentic Device on
|
|
22
|
+
// that phone, which sends the code on to the watch instead of pairing itself. The code inside is the same.
|
|
23
|
+
export const WATCH_PAIR_LINK = "https://intentic.dev/watch/pair";
|
|
24
|
+
|
|
21
25
|
const base64url = (value: string): string => btoa(value).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
22
26
|
|
|
23
27
|
export const phonePairingCode = (pairing: { readonly url: string; readonly token: string }): string =>
|
|
@@ -26,12 +30,17 @@ export const phonePairingCode = (pairing: { readonly url: string; readonly token
|
|
|
26
30
|
export const phonePairingLink = (pairing: { readonly url: string; readonly token: string }): string =>
|
|
27
31
|
`${PHONE_PAIR_LINK}#${phonePairingCode(pairing)}`;
|
|
28
32
|
|
|
33
|
+
export const watchPairingLink = (pairing: { readonly url: string; readonly token: string }): string =>
|
|
34
|
+
`${WATCH_PAIR_LINK}#${phonePairingCode(pairing)}`;
|
|
35
|
+
|
|
29
36
|
// The phone's side, and the editor's for a pasted code; undefined for anything not one of ours. Accepts the bare code
|
|
30
|
-
// or the whole link, since a person may paste either.
|
|
37
|
+
// or the whole link (a phone's or a watch's), since a person may paste either.
|
|
31
38
|
export const parsePhonePairingCode = (input: string): { readonly url: string; readonly token: string } | undefined => {
|
|
32
39
|
const trimmed = input.trim();
|
|
33
40
|
// The site serves the page with a trailing slash, so a link copied out of the browser may carry one.
|
|
34
|
-
const linked = [`${PHONE_PAIR_LINK}#`, `${PHONE_PAIR_LINK}/#`].find((prefix) =>
|
|
41
|
+
const linked = [`${PHONE_PAIR_LINK}#`, `${PHONE_PAIR_LINK}/#`, `${WATCH_PAIR_LINK}#`, `${WATCH_PAIR_LINK}/#`].find((prefix) =>
|
|
42
|
+
trimmed.startsWith(prefix),
|
|
43
|
+
);
|
|
35
44
|
const code = linked === undefined ? trimmed : trimmed.slice(linked.length);
|
|
36
45
|
if (!code.startsWith(PAIRING_PREFIX)) {
|
|
37
46
|
return undefined;
|
|
@@ -4,7 +4,7 @@ import { packageRoot } from "@intentic/constants/node";
|
|
|
4
4
|
import { OkSchema } from "../../schemas/shared.js";
|
|
5
5
|
import { PhoneFactsSchema, PhoneScopesSchema } from "../../schemas/phone.js";
|
|
6
6
|
import { PHONE_ERROR, PhoneHelloSchema, PhoneRequestSchema, PhoneResponseSchema } from "./phone-protocol.js";
|
|
7
|
-
import { parsePhonePairingCode, phoneConnectUrl, phoneEnrollUrl, phonePairingCode, phonePairingLink } from "./phone-links.js";
|
|
7
|
+
import { parsePhonePairingCode, phoneConnectUrl, phoneEnrollUrl, phonePairingCode, phonePairingLink, watchPairingLink } from "./phone-links.js";
|
|
8
8
|
|
|
9
9
|
// The phone door's wire as examples, spelled once here: golden/phone-wire.json is this file's `wire` as JSON, and both
|
|
10
10
|
// far ends test against it — the daemon's JSON-RPC link (_sandbox/sandbox/src/phones/json-rpc-link.test.ts) sends
|
|
@@ -123,6 +123,13 @@ test("a pairing round-trips as a code and as the link the QR carries", () => {
|
|
|
123
123
|
expect(parsePhonePairingCode(phonePairingLink(pairing).replace("/pair#", "/pair/#"))).toEqual(pairing);
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
test("a watch's link carries the same code, under the path its phone hands on", () => {
|
|
127
|
+
const pairing = { url: "https://sandbox-abc.sbx.intentic.dev", token: "one-time" };
|
|
128
|
+
expect(watchPairingLink(pairing)).toBe(`https://intentic.dev/watch/pair#${phonePairingCode(pairing)}`);
|
|
129
|
+
expect(parsePhonePairingCode(watchPairingLink(pairing))).toEqual(pairing);
|
|
130
|
+
expect(parsePhonePairingCode(watchPairingLink(pairing).replace("/pair#", "/pair/#"))).toEqual(pairing);
|
|
131
|
+
});
|
|
132
|
+
|
|
126
133
|
test("a code that is not ours, or carries no usable url or token, is refused rather than attempted", () => {
|
|
127
134
|
expect(parsePhonePairingCode("ixb1_abc")).toBeUndefined();
|
|
128
135
|
expect(parsePhonePairingCode("ixp1_not-base64!")).toBeUndefined();
|
|
@@ -18,6 +18,19 @@ export const webextSessionUrl = (sandboxUrl: string): string => `${sandboxUrl.re
|
|
|
18
18
|
// an MCP result.
|
|
19
19
|
export const webextLendUrl = (sandboxUrl: string): string => `${sandboxUrl.replace(/\/$/, "")}/system/webext/lend`;
|
|
20
20
|
|
|
21
|
+
// Where the extension collects a stored secret it was handed a ticket to type. Its own door for the reason the two above
|
|
22
|
+
// are: a tool answer over the socket lands in the model's context, and this value must never reach one.
|
|
23
|
+
export const webextSecretUrl = (sandboxUrl: string): string => `${sandboxUrl.replace(/\/$/, "")}/system/webext/secret`;
|
|
24
|
+
|
|
25
|
+
// The tool the daemon calls on a browser to type a secret into its focused field, taking only a ticket the daemon
|
|
26
|
+
// minted for that one call. The agent's own mount of the browser never lists it (webext-peer.ts): a model holding the
|
|
27
|
+
// tool would hold nothing it could use, so it is not offered.
|
|
28
|
+
export const WEBEXT_TICKET_TOOL = "type_ticket";
|
|
29
|
+
|
|
30
|
+
// How long a ticket waits for its browser to collect it. Both sides read it: the extension's own question to the person
|
|
31
|
+
// has to be answered inside it, or the ticket it then presents has already lapsed.
|
|
32
|
+
export const WEBEXT_TICKET_TTL_MS = 60_000;
|
|
33
|
+
|
|
21
34
|
// One pasteable code, not two fields: base64url of {url, token}, since a popup gets one paste, not two boxes. `token`
|
|
22
35
|
// is single-use and expires in ten minutes; the prefix versions the format.
|
|
23
36
|
const PAIRING_PREFIX = "ixb1_";
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
],
|
|
47
47
|
"bulk": [
|
|
48
48
|
"GET /diff/raw",
|
|
49
|
+
"GET /agents/{id}/documents/raw",
|
|
49
50
|
"GET /workspace/raw",
|
|
50
51
|
"GET /workspace/media",
|
|
51
52
|
"GET /workspace/download",
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
"lanes": [
|
|
62
63
|
{ "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/workspace/media?path=a.mp4", "lane": "bulk" },
|
|
63
64
|
{ "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/workspace/download?ticket=t", "lane": "bulk" },
|
|
65
|
+
{ "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/agents/c1/documents/raw?path=letter.odt", "lane": "bulk" },
|
|
64
66
|
{ "host": "sandbox-abcdef012345.sbx.example.test", "method": "POST", "path": "/workspace/upload?path=a", "lane": "bulk" },
|
|
65
67
|
{ "host": "sandbox-abcdef012345.sbx.example.test:443", "method": "GET", "path": "/system/runners/git/app/info/refs?service=git-upload-pack", "lane": "bulk" },
|
|
66
68
|
{ "host": "sandbox-abcdef012345.sbx.example.test", "method": "POST", "path": "/system/runners/git/app/git-receive-pack", "lane": "bulk" },
|
|
@@ -68,7 +68,7 @@ export const redeemWhileWarming = async (
|
|
|
68
68
|
return { kind: "answered", response };
|
|
69
69
|
} catch (error) {
|
|
70
70
|
if (signal?.aborted === true) {
|
|
71
|
-
//
|
|
71
|
+
// the stop's own reason, as an aborted fetch rejects with it
|
|
72
72
|
throw signal.reason;
|
|
73
73
|
}
|
|
74
74
|
if (error instanceof Warming) {
|
|
@@ -158,11 +158,11 @@ test("a far end that never answers is reported a few times, then retried quietly
|
|
|
158
158
|
try {
|
|
159
159
|
const { link, sockets, said } = dialling(60_000);
|
|
160
160
|
for (let attempt = 1; attempt <= 20; attempt += 1) {
|
|
161
|
-
//
|
|
161
|
+
// one attempt after another is the thing under test
|
|
162
162
|
await waitFor(() => expect(sockets).toHaveLength(attempt));
|
|
163
163
|
sockets.at(-1)?.errors();
|
|
164
164
|
sockets.at(-1)?.drops(1006);
|
|
165
|
-
//
|
|
165
|
+
// the ladder's own wait, serial by construction
|
|
166
166
|
await advanceTimersByTimeAsync(60_000);
|
|
167
167
|
}
|
|
168
168
|
|
|
@@ -197,10 +197,10 @@ test("a link that comes back is loud again about the outage after it", async ()
|
|
|
197
197
|
try {
|
|
198
198
|
const { link, sockets, said } = dialling();
|
|
199
199
|
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
200
|
-
//
|
|
200
|
+
// one attempt after another is the thing under test
|
|
201
201
|
await waitFor(() => expect(sockets).toHaveLength(attempt));
|
|
202
202
|
sockets.at(-1)?.drops(1006);
|
|
203
|
-
//
|
|
203
|
+
// the ladder's own wait, serial by construction
|
|
204
204
|
await advanceTimersByTimeAsync(1_000);
|
|
205
205
|
}
|
|
206
206
|
const whileQuiet = said.length;
|
|
@@ -273,10 +273,10 @@ test("a link nobody has answered in a long time rests between tries, and is neve
|
|
|
273
273
|
const { link, sockets, said } = dialling(RUNG_MS);
|
|
274
274
|
// Up to the threshold on the ordinary ladder: drop, wait its rung, get the next socket.
|
|
275
275
|
for (let attempt = 1; attempt < LONG_OUTAGE_ATTEMPTS; attempt += 1) {
|
|
276
|
-
//
|
|
276
|
+
// one drop per attempt is serial by definition
|
|
277
277
|
await waitFor(() => expect(sockets).toHaveLength(attempt));
|
|
278
278
|
sockets.at(-1)?.drops(1006);
|
|
279
|
-
//
|
|
279
|
+
// ditto
|
|
280
280
|
await advanceTimersByTimeAsync(RUNG_MS);
|
|
281
281
|
}
|
|
282
282
|
await waitFor(() => expect(sockets).toHaveLength(LONG_OUTAGE_ATTEMPTS));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errorMessage } from "@intentic/base/errors";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { createMcpServer, type McpAuditEntry, textResult, tool } from "./peer-mcp-server.js";
|
|
3
4
|
|
|
@@ -32,7 +33,7 @@ const build = () => {
|
|
|
32
33
|
],
|
|
33
34
|
noSuchTool: (name) => `This peer has no tool called "${name}".`,
|
|
34
35
|
refused: (error) => error instanceof Refused,
|
|
35
|
-
errorMessage
|
|
36
|
+
errorMessage,
|
|
36
37
|
audit: (entry) => void audits.push(entry),
|
|
37
38
|
});
|
|
38
39
|
return { handle, audits };
|
|
@@ -19,7 +19,7 @@ import { MemberEmailSchema, MemberGrantSchema, MemberSelfSchema, MembersListSche
|
|
|
19
19
|
import { PhonesListSchema, PhoneWakeRegistrationSchema } from "../../schemas/phone.js";
|
|
20
20
|
import { OkSchema } from "../../schemas/shared.js";
|
|
21
21
|
import { EnrollCodeAnswerSchema, EnrollCodeRequestSchema, PeerPairingAnswerSchema, SyncPairAnswerSchema, SyncPairRequestSchema } from "../../schemas/short-codes.js";
|
|
22
|
-
import { SpeechStatusSchema, SpeechTranscriptSchema } from "../../schemas/speech.js";
|
|
22
|
+
import { SpeechStatusSchema, SpeechTranscriptSchema, SpeechWakeClaimSchema, SpeechWakeGrantSchema } from "../../schemas/speech.js";
|
|
23
23
|
import {
|
|
24
24
|
AuthenticationOptionsSchema,
|
|
25
25
|
DaemonSessionSchema,
|
|
@@ -35,6 +35,20 @@ import {
|
|
|
35
35
|
SessionRecoveredSchema,
|
|
36
36
|
} from "../../schemas/system.js";
|
|
37
37
|
import { WebExtsListSchema } from "../../schemas/webext.js";
|
|
38
|
+
import {
|
|
39
|
+
InboundLinkMintedSchema,
|
|
40
|
+
InboundLinkMintSchema,
|
|
41
|
+
InboundLinksListSchema,
|
|
42
|
+
InboundLinkSummarySchema,
|
|
43
|
+
InboundLinkTermsUpdateSchema,
|
|
44
|
+
LinkAnswerSchema,
|
|
45
|
+
LinkHelloSchema,
|
|
46
|
+
LinkMessageReceiptSchema,
|
|
47
|
+
LinkMessageSchema,
|
|
48
|
+
LinkRequestDecisionSchema,
|
|
49
|
+
LinkTaskSchema,
|
|
50
|
+
LinkTaskStatusSchema,
|
|
51
|
+
} from "../../schemas/links.js";
|
|
38
52
|
import { UploadManifestSchema, UploadSkipSchema } from "../../schemas/workspace/workspace-upload.js";
|
|
39
53
|
import {
|
|
40
54
|
ArrivalAbandonedSchema,
|
|
@@ -128,6 +142,20 @@ export const RAW_JSON_ROUTES = {
|
|
|
128
142
|
"DELETE /system/authorized-key/{machine}": { output: OkSchema },
|
|
129
143
|
// Every browser session revoked, the owner's own included.
|
|
130
144
|
"POST /system/sessions/revoke": { output: OkSchema },
|
|
145
|
+
// Inbound links: the roster, a mint (its token shown once), new terms, a revoke, and the owner's answer to a waiting task.
|
|
146
|
+
"GET /system/links": { output: InboundLinksListSchema },
|
|
147
|
+
"POST /system/links": { input: InboundLinkMintSchema, output: InboundLinkMintedSchema },
|
|
148
|
+
"POST /system/links/{id}": { input: InboundLinkTermsUpdateSchema, output: InboundLinkSummarySchema },
|
|
149
|
+
"DELETE /system/links/{id}": { output: OkSchema },
|
|
150
|
+
"POST /system/links/{id}/requests/{request}": { input: LinkRequestDecisionSchema, output: InboundLinkSummarySchema },
|
|
151
|
+
// The link doors, as the asking sandbox's daemon reads them. A status takes `?after=<version>&wait=<seconds>` to
|
|
152
|
+
// be held until it moves past that version.
|
|
153
|
+
"GET /link": { output: LinkHelloSchema },
|
|
154
|
+
"POST /link/conversations": { input: LinkTaskSchema, output: LinkTaskStatusSchema },
|
|
155
|
+
"GET /link/conversations/{id}": { output: LinkTaskStatusSchema },
|
|
156
|
+
"POST /link/conversations/{id}/messages": { input: LinkMessageSchema, output: LinkMessageReceiptSchema },
|
|
157
|
+
"POST /link/conversations/{id}/answer": { input: LinkAnswerSchema, output: OkSchema },
|
|
158
|
+
"POST /link/conversations/{id}/stop": { output: LinkTaskStatusSchema },
|
|
131
159
|
"GET /system/control/tokens": { output: ControlTokensListSchema },
|
|
132
160
|
"POST /system/control/tokens": { input: ControlTokenMintSchema, output: ControlTokenMintedSchema },
|
|
133
161
|
"DELETE /system/control/tokens/{id}": { output: OkSchema },
|
|
@@ -151,6 +179,8 @@ export const RAW_JSON_ROUTES = {
|
|
|
151
179
|
"GET /speech/status": { output: SpeechStatusSchema },
|
|
152
180
|
"POST /speech/prepare": { output: SpeechStatusSchema },
|
|
153
181
|
"POST /speech/transcribe": { bytes: true, output: SpeechTranscriptSchema },
|
|
182
|
+
// One device's claim on a wake word it heard, so two devices hearing the same "Leon" open one session between them.
|
|
183
|
+
"POST /speech/wake": { input: SpeechWakeClaimSchema, output: SpeechWakeGrantSchema },
|
|
154
184
|
// A file written into the tree (`?path=`, the body its bytes, `x-intentic-base-hash` refusing a stale save), and the
|
|
155
185
|
// pre-drop check of which dropped files are already there.
|
|
156
186
|
"POST /workspace/upload": { bytes: true, output: OkSchema },
|