@intentic/sandbox-contract 1.169.0 → 1.171.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/dist/agent-catalog.d.ts +1 -0
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/agent-catalog.js +2 -1
- package/dist/agent-catalog.js.map +1 -1
- package/dist/chores/chores.d.ts +45 -0
- package/dist/chores/chores.d.ts.map +1 -0
- package/dist/chores/chores.js +487 -0
- package/dist/chores/chores.js.map +1 -0
- package/dist/chores/digest.d.ts +3 -0
- package/dist/chores/digest.d.ts.map +1 -0
- package/dist/chores/digest.js +0 -0
- package/dist/chores/digest.js.map +1 -0
- package/dist/chores/index.d.ts +10 -0
- package/dist/chores/index.d.ts.map +1 -0
- package/dist/chores/index.js +6 -0
- package/dist/chores/index.js.map +1 -0
- package/dist/chores/probes.d.ts +15 -0
- package/dist/chores/probes.d.ts.map +1 -0
- package/dist/chores/probes.js +177 -0
- package/dist/chores/probes.js.map +1 -0
- package/dist/chores/prompt.d.ts +14 -0
- package/dist/chores/prompt.d.ts.map +1 -0
- package/dist/chores/prompt.js +12 -0
- package/dist/chores/prompt.js.map +1 -0
- package/dist/chores/verdict.d.ts +20 -0
- package/dist/chores/verdict.d.ts.map +1 -0
- package/dist/chores/verdict.js +59 -0
- package/dist/chores/verdict.js.map +1 -0
- package/dist/contracts/agent.contract.d.ts +22 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +2 -1
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +2 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/automations.contract.d.ts +54 -0
- package/dist/contracts/automations.contract.d.ts.map +1 -1
- package/dist/contracts/chores.contract.d.ts +151 -0
- package/dist/contracts/chores.contract.d.ts.map +1 -0
- package/dist/contracts/chores.contract.js +8 -0
- package/dist/contracts/chores.contract.js.map +1 -0
- package/dist/contracts/extensions.contract.d.ts +7 -0
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/extensions.contract.js +2 -1
- package/dist/contracts/extensions.contract.js.map +1 -1
- package/dist/contracts/system.contract.d.ts +14 -14
- package/dist/contracts/workspace.contract.d.ts +11 -24
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/events.d.ts +10 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +7 -1
- package/dist/events.js.map +1 -1
- package/dist/hostnames.d.ts +0 -1
- package/dist/hostnames.d.ts.map +1 -1
- package/dist/hostnames.js +0 -1
- package/dist/hostnames.js.map +1 -1
- package/dist/index.d.ts +261 -38
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/schemas.d.ts +618 -40
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +145 -2
- package/dist/schemas.js.map +1 -1
- package/dist/tunnel-ids.d.ts +2 -0
- package/dist/tunnel-ids.d.ts.map +1 -1
- package/dist/tunnel-ids.js +2 -0
- package/dist/tunnel-ids.js.map +1 -1
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +9 -0
- package/dist/workspace-state.js.map +1 -1
- package/package.json +14 -2
- package/src/agent-catalog.test.ts +32 -1
- package/src/agent-catalog.ts +25 -7
- package/src/chores/chores.ts +837 -0
- package/src/chores/digest.test.ts +30 -0
- package/src/chores/digest.ts +0 -0
- package/src/chores/index.ts +9 -0
- package/src/chores/probes.test.ts +166 -0
- package/src/chores/probes.ts +273 -0
- package/src/chores/prompt.ts +64 -0
- package/src/chores/verdict.test.ts +394 -0
- package/src/chores/verdict.ts +167 -0
- package/src/contracts/agent.contract.ts +14 -1
- package/src/contracts/chores.contract.ts +23 -0
- package/src/contracts/extensions.contract.ts +5 -0
- package/src/events.ts +15 -2
- package/src/hostnames.ts +4 -6
- package/src/index.ts +3 -0
- package/src/schemas.ts +427 -19
- package/src/tunnel-ids.test.ts +49 -0
- package/src/tunnel-ids.ts +24 -0
- package/src/workspace-state.ts +13 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { hostSshIdFromToken, PORT_SLOT_COUNT, portSlotsFromToken, sandboxIdFromToken } from "./tunnel-ids.js";
|
|
3
|
+
|
|
4
|
+
const TOKEN = "connect-token-one";
|
|
5
|
+
const OTHER = "connect-token-two";
|
|
6
|
+
|
|
7
|
+
test("the sandbox id is a stable 12-hex digest of the connect token, and absent without one", () => {
|
|
8
|
+
expect(sandboxIdFromToken(TOKEN)).toMatch(/^[0-9a-f]{12}$/);
|
|
9
|
+
expect(sandboxIdFromToken(TOKEN)).toBe(sandboxIdFromToken(TOKEN));
|
|
10
|
+
expect(sandboxIdFromToken(OTHER)).not.toBe(sandboxIdFromToken(TOKEN));
|
|
11
|
+
expect(sandboxIdFromToken("")).toBeUndefined();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("a host ssh id is salted per host, so two deploy targets never collide", () => {
|
|
15
|
+
expect(hostSshIdFromToken(TOKEN, "web-1")).toMatch(/^[0-9a-f]{12}$/);
|
|
16
|
+
expect(hostSshIdFromToken(TOKEN, "web-1")).not.toBe(hostSshIdFromToken(TOKEN, "web-2"));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("port slots are a fixed-size pool of DNS-safe labels, stable per token", () => {
|
|
20
|
+
const slots = portSlotsFromToken(TOKEN);
|
|
21
|
+
// Fixed size: the pool is the cap on preview DNS records a sandbox costs the shared zone.
|
|
22
|
+
expect(slots).toHaveLength(PORT_SLOT_COUNT);
|
|
23
|
+
// Every label must survive as one DNS label AND pass the platform's mint filter
|
|
24
|
+
// (/^(preview|port)-[a-z0-9][a-z0-9-]*$/, ≤50 chars), which `port-<slot>` has to satisfy.
|
|
25
|
+
for (const slot of slots) {
|
|
26
|
+
expect(slot).toMatch(/^[0-9a-f]{12}$/);
|
|
27
|
+
expect(`port-${slot}`).toMatch(/^port-[a-z0-9][a-z0-9-]*$/);
|
|
28
|
+
expect(`port-${slot}`.length).toBeLessThanOrEqual(50);
|
|
29
|
+
}
|
|
30
|
+
// Distinct, or two forwards would fight over one hostname.
|
|
31
|
+
expect(new Set(slots).size).toBe(PORT_SLOT_COUNT);
|
|
32
|
+
// Stable: the daemon and the platform derive these independently and must agree, and a restart has to
|
|
33
|
+
// land on the records already minted rather than orphaning them.
|
|
34
|
+
expect(portSlotsFromToken(TOKEN)).toEqual(slots);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/* The whole point of the salt. The sandbox id is public — it is the leading label of the sandbox's own URL and
|
|
38
|
+
* of every preview link its owner has shared — so anything derived from the id ALONE is derivable by whoever
|
|
39
|
+
* holds one of those links. Slots must not be: knowing a sandbox's id must not tell you where its forwarded
|
|
40
|
+
* ports live. */
|
|
41
|
+
test("slots are not derivable from the sandbox id — only from the token behind it", () => {
|
|
42
|
+
expect(portSlotsFromToken(OTHER)).not.toEqual(portSlotsFromToken(TOKEN));
|
|
43
|
+
// No slot leaks the id it will be paired with in `port-<slot>-<sandboxId>`.
|
|
44
|
+
const id = sandboxIdFromToken(TOKEN);
|
|
45
|
+
expect(id).toBeDefined();
|
|
46
|
+
expect(portSlotsFromToken(TOKEN)).not.toContain(id);
|
|
47
|
+
// And none of them is the old fixed alphabet, which is what made the hostnames guessable.
|
|
48
|
+
expect(portSlotsFromToken(TOKEN).some((slot) => slot.length === 1)).toBe(false);
|
|
49
|
+
});
|
package/src/tunnel-ids.ts
CHANGED
|
@@ -17,3 +17,27 @@ export const sandboxIdFromToken = (connectToken: string): string | undefined =>
|
|
|
17
17
|
// own ssh-<id>.<zone> (no collision across hosts). Shared by the CLI (createHostSshTunnel) and the platform API
|
|
18
18
|
// (provisionHostSshTunnel), which MUST derive the identical id.
|
|
19
19
|
export const hostSshIdFromToken = (connectToken: string, hostName: string): string => sha256Hex(`${connectToken}:${hostName}`).slice(0, 12);
|
|
20
|
+
|
|
21
|
+
/* How many port-forward slots a sandbox has. Eight is enough for a monorepo's worth of concurrent dev servers,
|
|
22
|
+
* and it is the hard cap on preview DNS records a sandbox can ever cost the shared intentic zone. */
|
|
23
|
+
export const PORT_SLOT_COUNT = 8;
|
|
24
|
+
|
|
25
|
+
/* THE PORT-FORWARD SLOT LABELS — the `port-<slot>` half of `port-<slot>-<sandboxId>.<zone>`.
|
|
26
|
+
*
|
|
27
|
+
* These were the letters a…h, and that was the hole: a forwarded port's hostname was then a pure function of the
|
|
28
|
+
* sandbox id, and the sandbox id is not a secret — it is the leading label of the URL the owner uses daily and
|
|
29
|
+
* of every preview link they have ever shared. So anyone who had seen ONE preview link could poll eight fixed
|
|
30
|
+
* names forever and catch whatever the owner forwarded, at any point in the future. The Ports view says a
|
|
31
|
+
* forwarded port is public, and it is; what it could not say was that "public" meant eight guessable URLs.
|
|
32
|
+
*
|
|
33
|
+
* Salting with the connect token fixes that without costing anything the letters bought. Still exactly eight
|
|
34
|
+
* records (the reason slots exist at all — the intentic-provided zone mints per label, and dev servers churn
|
|
35
|
+
* ephemeral ports far faster than DNS should), still stable across restarts so a slot's record stays warm, and
|
|
36
|
+
* still derivable with no coordination by every party that already holds the token: the daemon that forwards,
|
|
37
|
+
* and the platform that mints the DNS. A party without the token has no business predicting these names.
|
|
38
|
+
*
|
|
39
|
+
* The browser is deliberately NOT one of those parties — it never derives a port hostname, it reads `previewUrl`
|
|
40
|
+
* off the daemon's response — which is why this can live here, in the node-only half of the contract, next to
|
|
41
|
+
* the digest it shares with sandboxIdFromToken. */
|
|
42
|
+
export const portSlotsFromToken = (connectToken: string): readonly string[] =>
|
|
43
|
+
Array.from({ length: PORT_SLOT_COUNT }, (_, index) => sha256Hex(`${connectToken}:port:${index}`).slice(0, 12));
|
package/src/workspace-state.ts
CHANGED
|
@@ -77,11 +77,24 @@ export const WORKSPACE_STATE_FILES: readonly WorkspaceStateFile[] = [
|
|
|
77
77
|
* Both entries below are outside that by design, so an empty set is the honest record — naming a key no
|
|
78
78
|
* query uses would put the drift this table exists to remove straight back into it. Each says which
|
|
79
79
|
* constraint would have to move first, so the next reader doesn't re-derive it. */
|
|
80
|
+
{
|
|
81
|
+
path: ".intentic/webchat-sessions.json",
|
|
82
|
+
invalidates: [],
|
|
83
|
+
why: "Doorbell thread bookkeeping (visitor thread → sandbox conversation + provider session), written on EVERY visitor message. Nothing in the browser reads it: what a visitor's chat produces is a conversation, and the fleet board already learns about that from the agent registry's own push. Naming a key here would bill every connected browser a refetch per inbound message — the request storm this table's own note warns about — to refresh nothing it can see.",
|
|
84
|
+
},
|
|
80
85
|
{
|
|
81
86
|
path: ".intentic/extension-settings.json",
|
|
82
87
|
invalidates: [],
|
|
83
88
|
why: "Held in a module-level shallowRef store per extension (web's extensionSettingsStore) with no query observer, and deliberately so: api.settings.get must answer SYNCHRONOUSLY from an extension's first activate() line, and the store outlives every component scope. A module-level QueryObserver is the one shape that would make invalidation refetch, and this app already ruled it out — it detaches on the queryClient.clear() at logout (see useSandbox's sandbox-list mirror). So a remote member's setting edit reaches this browser on its next load, not live.",
|
|
84
89
|
},
|
|
90
|
+
/* Unlike the settings file above it, the on/off switch IS observed by a query — the Extensions tab's list,
|
|
91
|
+
* which carries each row's switch position — so a flip made elsewhere (another member, the agent writing the
|
|
92
|
+
* file) shows up here live. It does not re-run the host: activating or retiring an extension is the loader's
|
|
93
|
+
* reconcile, which the tab's own toggle triggers, so a remote flip takes effect on this browser's next load. */
|
|
94
|
+
{
|
|
95
|
+
path: ".intentic/extension-enablement.json",
|
|
96
|
+
invalidates: ["extensions"],
|
|
97
|
+
},
|
|
85
98
|
{
|
|
86
99
|
path: ".intentic/members.json",
|
|
87
100
|
invalidates: [],
|