@intentic/sandbox-contract 1.222.0 → 1.224.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/chores/chores.js +1 -1
- package/dist/chores/chores.js.map +1 -1
- package/dist/chores/prompt.d.ts.map +1 -1
- package/dist/chores/prompt.js +3 -3
- package/dist/chores/prompt.js.map +1 -1
- package/dist/contracts/ports.contract.d.ts +11 -0
- package/dist/contracts/ports.contract.d.ts.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/schemas.d.ts +22 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +3 -0
- package/dist/schemas.js.map +1 -1
- package/package.json +4 -4
- package/src/agent-catalog.ts +78 -78
- package/src/agent-run-model.ts +8 -8
- package/src/capability-env.ts +3 -3
- package/src/capability-secrets.ts +5 -5
- package/src/chores/chores.ts +67 -67
- package/src/chores/digest.ts +3 -3
- package/src/chores/extension-update.ts +3 -3
- package/src/chores/fix-deps.ts +3 -3
- package/src/chores/probes.ts +16 -16
- package/src/chores/prompt.ts +9 -9
- package/src/chores/stack.ts +17 -17
- package/src/chores/verdict.ts +23 -23
- package/src/contract-lock.ts +7 -7
- package/src/contracts/activity.contract.ts +1 -1
- package/src/contracts/agent.contract.ts +2 -2
- package/src/contracts/agents.contract.ts +11 -11
- package/src/contracts/automations.contract.ts +5 -5
- package/src/contracts/capabilities.contract.ts +7 -7
- package/src/contracts/chores.contract.ts +3 -3
- package/src/contracts/ci.contract.ts +2 -2
- package/src/contracts/claude.contract.ts +3 -3
- package/src/contracts/drafts.contract.ts +1 -1
- package/src/contracts/endpoints.contract.ts +2 -2
- package/src/contracts/extensions.contract.ts +8 -8
- package/src/contracts/git.contract.ts +6 -6
- package/src/contracts/grok.contract.ts +4 -4
- package/src/contracts/host.contract.ts +5 -5
- package/src/contracts/intentic.contract.ts +2 -2
- package/src/contracts/logs.contract.ts +1 -1
- package/src/contracts/loops.contract.ts +8 -8
- package/src/contracts/personas.contract.ts +8 -8
- package/src/contracts/ports.contract.ts +1 -1
- package/src/contracts/prepush.contract.ts +2 -2
- package/src/contracts/providers.contract.ts +4 -4
- package/src/contracts/public.contract.ts +1 -1
- package/src/contracts/push.contract.ts +2 -2
- package/src/contracts/secrets.contract.ts +2 -2
- package/src/contracts/sessions.contract.ts +2 -2
- package/src/contracts/settings.contract.ts +3 -3
- package/src/contracts/share.contract.ts +1 -1
- package/src/contracts/skills.contract.ts +2 -2
- package/src/contracts/system.contract.ts +9 -9
- package/src/contracts/usage.contract.ts +1 -1
- package/src/contracts/vpn.contract.ts +5 -5
- package/src/contracts/workflows.contract.ts +12 -12
- package/src/contracts/workspace.contract.ts +13 -13
- package/src/conversation-ids.ts +7 -7
- package/src/events.ts +136 -136
- package/src/history-state.ts +12 -12
- package/src/host-protocol.ts +7 -7
- package/src/hostnames.ts +15 -15
- package/src/index.ts +8 -8
- package/src/listener-protocol.ts +12 -12
- package/src/model-order.ts +30 -30
- package/src/output-fields.ts +9 -9
- package/src/path-refs.ts +6 -6
- package/src/publish-drafts.ts +6 -6
- package/src/quick-model.ts +15 -15
- package/src/routes.ts +19 -19
- package/src/runtime-state.ts +22 -22
- package/src/schemas.ts +930 -918
- package/src/search-globs.ts +6 -6
- package/src/session-names.ts +5 -5
- package/src/share-paths.ts +7 -7
- package/src/sse.ts +1 -1
- package/src/state-portability.ts +8 -8
- package/src/terminal-protocol.ts +3 -3
- package/src/title.ts +20 -20
- package/src/tunnel-ids.ts +7 -7
- package/src/versions.ts +5 -5
- package/src/workflow-faults.ts +11 -11
- package/src/workspace-state.ts +119 -119
package/src/search-globs.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
/* What the search box's second field MEANS
|
|
1
|
+
/* What the search box's second field MEANS. VSCode's "files to include" grammar, read the way VSCode reads it
|
|
2
2
|
* (its queryBuilder's parseSearchPaths + expandGlobalGlob), and answered as the two path-glob lists the search
|
|
3
3
|
* engine takes.
|
|
4
4
|
*
|
|
5
|
-
* The rules that make it feel like the editor's field, each one
|
|
5
|
+
* The rules that make it feel like the editor's field, each one required:
|
|
6
6
|
*
|
|
7
|
-
* `package.json` a bare name is a FILE as well as a folder
|
|
7
|
+
* `package.json` a bare name is a FILE as well as a folder, every segment expands to BOTH `**\/p` and
|
|
8
8
|
* `**\/p/**`. Reading it as a folder alone is what made a file name find nothing.
|
|
9
|
-
* `src/db` a path is still matched at any depth, not anchored
|
|
9
|
+
* `src/db` a path is still matched at any depth, not anchored, `**\/src/db`. VSCode only anchors
|
|
10
10
|
* `./src/db` when the segment starts with `./` (or `/`), which is how you say "the one at the root".
|
|
11
11
|
* `.ts` a leading dot is shorthand for the extension: it becomes `*.ts`.
|
|
12
12
|
* `docs/` a trailing slash is noise; the folder form is generated either way.
|
|
13
13
|
* `*.{ts,vue}` commas separate patterns EXCEPT inside `{…}` or `[…]`, which are one pattern's own syntax.
|
|
14
|
-
* `!**\/*.spec.ts` a leading `!` excludes. This is ours, not VSCode's
|
|
14
|
+
* `!**\/*.spec.ts` a leading `!` excludes. This is ours, not VSCode's, the editor spends a second box on
|
|
15
15
|
* exclusions and the explorer's sidebar has room for one field.
|
|
16
16
|
*
|
|
17
17
|
* It lives in the contract package because both ends run it: the daemon turns it into engine scope, and the
|
|
@@ -60,7 +60,7 @@ const expand = (segment: string): string[] => {
|
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
export interface IncludeGlobs {
|
|
63
|
-
// Files the search is limited to
|
|
63
|
+
// Files the search is limited to, empty means the whole workspace. OR'ed: any pattern matching admits.
|
|
64
64
|
readonly globs: readonly string[];
|
|
65
65
|
// Files kept out of it, whatever the includes said.
|
|
66
66
|
readonly notGlobs: readonly string[];
|
package/src/session-names.ts
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
*
|
|
3
3
|
* A turn that runs Bash gets a tmux session; a turn that browses gets a Chromium the daemon watches over CDP.
|
|
4
4
|
* Both are named off the SDK session id by the same derivation, and both are listed together by
|
|
5
|
-
* GET /system/terminals
|
|
5
|
+
* GET /system/terminals, so the name is wire vocabulary, not an implementation detail of either half.
|
|
6
6
|
*
|
|
7
7
|
* It lives in the contract because three parties derive it and none of them may guess: the daemon NAMES the
|
|
8
8
|
* session, the web app OPENS it, and an extension that wants to point the user at a turn's live browser has only
|
|
9
9
|
* the fleet roster's `sessionId` to work from. A second copy of this string math anywhere is a drift waiting to
|
|
10
|
-
* happen
|
|
10
|
+
* happen, the copy would keep resolving to a plausible name that nothing has ever listed.
|
|
11
11
|
*
|
|
12
|
-
* Reached as `@intentic/sandbox-contract/session-names`, NOT through the barrel
|
|
12
|
+
* Reached as `@intentic/sandbox-contract/session-names`, NOT through the barrel, the same shape tunnel-ids has,
|
|
13
13
|
* and for the same reason: this module has no dependencies, while the barrel pulls the whole contract (and
|
|
14
14
|
* @intentic/extension-api behind it). One subpath keeps a daemon unit test that only wants a session name from
|
|
15
15
|
* having to resolve the entire wire surface.
|
|
@@ -21,7 +21,7 @@ export const JOB_SESSION_PREFIX = "job-";
|
|
|
21
21
|
export const BROWSER_SESSION_PREFIX = "browser-";
|
|
22
22
|
|
|
23
23
|
/* Eight characters of the SDK session UUID, sanitized to the session-name charset. Eight is what groups a whole
|
|
24
|
-
* turn's work
|
|
24
|
+
* turn's work, including its subagents', under one name, and it clears the name guard
|
|
25
25
|
* (`^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$`) without further escaping. Undefined when the id sanitizes to empty, which
|
|
26
26
|
* is never a valid session name. */
|
|
27
27
|
export const sessionSuffix = (sessionId: string): string | undefined => {
|
|
@@ -29,7 +29,7 @@ export const sessionSuffix = (sessionId: string): string | undefined => {
|
|
|
29
29
|
return id === "" ? undefined : id;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
// The tmux session one SDK session's Bash commands run in
|
|
32
|
+
// The tmux session one SDK session's Bash commands run in, the same derivation the Bash hook routes commands
|
|
33
33
|
// through, so an emitted `terminal` frame and the live session can't drift.
|
|
34
34
|
export const agentSessionName = (sessionId: string): string | undefined => {
|
|
35
35
|
const suffix = sessionSuffix(sessionId);
|
package/src/share-paths.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* WHERE A SHARED CONVERSATION LIVES INSIDE THE OUTBOX
|
|
1
|
+
/* WHERE A SHARED CONVERSATION LIVES INSIDE THE OUTBOX, one layout, stated once, because three separate
|
|
2
2
|
* things have to agree on it and only one of them is code that runs at the same time as the others.
|
|
3
3
|
*
|
|
4
4
|
* public/
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* index.html the page, with its conversation baked in
|
|
9
9
|
* files/ the pictures that conversation showed, copied out of the workspace
|
|
10
10
|
*
|
|
11
|
-
* The VIEWER's build has to know its own address before it is ever published (its asset URLs are absolute
|
|
12
|
-
* `/conversations/_viewer/assets
|
|
11
|
+
* The VIEWER's build has to know its own address before it is ever published (its asset URLs are absolute,
|
|
12
|
+
* `/conversations/_viewer/assets/…`, so that one copy of the assets serves every share and a recipient's
|
|
13
13
|
* browser caches them across links). The DAEMON has to write the tree. And the outbox's own rules have to
|
|
14
14
|
* leave all of it alone: nothing here begins with a dot, which is the one shape that is never served.
|
|
15
15
|
*
|
|
16
|
-
* A directory per share rather than a bare `<id>.html`, so a share owns its pictures
|
|
16
|
+
* A directory per share rather than a bare `<id>.html`, so a share owns its pictures, "stop sharing" is then
|
|
17
17
|
* one directory removed, and cannot half-succeed by leaving a folder of someone's screenshots behind. */
|
|
18
18
|
|
|
19
19
|
// Top-level folder inside `public/`. Reads as what it is in the address bar, which matters: this is a string
|
|
@@ -24,7 +24,7 @@ export const SHARE_DIR = "conversations";
|
|
|
24
24
|
// machinery rather than as somebody's conversation.
|
|
25
25
|
export const SHARE_VIEWER_DIR = "_viewer";
|
|
26
26
|
|
|
27
|
-
// Where a share's pictures sit, relative to its own page
|
|
27
|
+
// Where a share's pictures sit, relative to its own page, and therefore also the prefix every rewritten
|
|
28
28
|
// image path in the payload carries.
|
|
29
29
|
export const SHARE_FILES_DIR = "files";
|
|
30
30
|
|
|
@@ -36,7 +36,7 @@ export const SHARE_VIEWER_BASE = `/${SHARE_DIR}/${SHARE_VIEWER_DIR}/`;
|
|
|
36
36
|
*
|
|
37
37
|
* Both halves earn their place. The stem is what makes a pasted link say what it points at ("…/conversations/
|
|
38
38
|
* login-redirect-fix-3f9c…"), which is most of why anyone trusts clicking one. The tail is the security: the
|
|
39
|
-
* outbox answers on an unguessable hostname AND requires the exact path (public-files.ts rule 4
|
|
39
|
+
* outbox answers on an unguessable hostname AND requires the exact path (public-files.ts rule 4, there is no
|
|
40
40
|
* listing), so an address is protected by the sum of the two, and the half this module controls must not be
|
|
41
41
|
* derivable from a title anyone could guess. */
|
|
42
42
|
const STEM_MAX = 48;
|
|
@@ -62,7 +62,7 @@ export const shareId = (title: string, random: string): string => {
|
|
|
62
62
|
return stem === "" ? random : `${stem}-${random}`;
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
-
// What a share's own id must look like before it is ever joined onto a path
|
|
65
|
+
// What a share's own id must look like before it is ever joined onto a path, the same shape the daemon's
|
|
66
66
|
// other id guards take (transcript-record.ts FILE_ID), and the reason a share id from the wire can be trusted
|
|
67
67
|
// into a directory name.
|
|
68
68
|
export const SHARE_ID = /^[a-z0-9][a-z0-9-]{0,95}$/;
|
package/src/sse.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Low-level SSE framing for the daemon's streamed routes (oRPC eventIterator over HTTP): frames separated by
|
|
2
|
-
// a blank line, each carrying one `data: <JSON>` line. Protocol-only (no domain shapes)
|
|
2
|
+
// a blank line, each carrying one `data: <JSON>` line. Protocol-only (no domain shapes), shared by every
|
|
3
3
|
// consumer of the wire: the web's chat/intentic streams and the ACP bridge's daemon client.
|
|
4
4
|
|
|
5
5
|
// Yields each raw SSE frame (the text between blank-line separators) as it arrives, reassembling frames split
|
package/src/state-portability.ts
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
/* WHETHER A PIECE OF DAEMON STATE TRAVELS
|
|
1
|
+
/* WHETHER A PIECE OF DAEMON STATE TRAVELS, the second thing every state manifest declares.
|
|
2
2
|
*
|
|
3
3
|
* A sandbox's environment is not one directory. `/work` holds the workspace and the daemon's manifests,
|
|
4
4
|
* `/history` holds the real git dirs, the fleet registry and the ledgers, and the container carries the built
|
|
5
5
|
* image and its env. "Export the environment" therefore means walking state that lives in two volumes and
|
|
6
|
-
* deciding, per entry, which of four things it is
|
|
6
|
+
* deciding, per entry, which of four things it is, and that decision has to be written down beside the entry
|
|
7
7
|
* itself, or the exporter becomes the hand-kept list that workspace-state.ts exists to argue against.
|
|
8
8
|
*
|
|
9
9
|
* So each manifest entry carries a class, and both the bundler and the restorer derive from it. Adding a store
|
|
10
10
|
* without saying whether it travels is a failing coverage test, not a file that silently goes missing from
|
|
11
11
|
* every bundle.
|
|
12
12
|
*
|
|
13
|
-
* The classes are exhaustive on purpose
|
|
13
|
+
* The classes are exhaustive on purpose, there is no "unset", because the failure mode of a default is
|
|
14
14
|
* exactly the one that matters: a credential store added later would inherit `carry` and ride out of the
|
|
15
15
|
* sandbox in a file the owner thought was safe to email.
|
|
16
16
|
*/
|
|
17
17
|
export type Portability =
|
|
18
18
|
/* Ordinary state. Travels in every bundle. */
|
|
19
19
|
| "carry"
|
|
20
|
-
/* Holds a credential
|
|
20
|
+
/* Holds a credential, a provider token, a webhook secret, an ssh key. Travels ONLY when the owner opts
|
|
21
21
|
* in at export, and the bundle records that it did. Never the default. */
|
|
22
22
|
| "secret"
|
|
23
23
|
/* Binds this sandbox to its owner, its browsers or its host: who may drive it, the session-signing secret,
|
|
24
|
-
* push subscriptions, enrollments, certificates. Never travels
|
|
24
|
+
* push subscriptions, enrollments, certificates. Never travels, the target mints its own, and carrying
|
|
25
25
|
* one would either hand the target's ownership to the source or leave two sandboxes claiming one identity. */
|
|
26
26
|
| "identity"
|
|
27
27
|
/* Regenerated by the target from state that does travel: composed files, caches, indexes, checkouts. Left
|
|
28
|
-
* out for size, not for safety
|
|
28
|
+
* out for size, not for safety, carrying it would only ship a stale copy of something the daemon rebuilds
|
|
29
29
|
* on its own. */
|
|
30
30
|
| "derived";
|
|
31
31
|
|
|
32
|
-
// One entry of a state manifest: the path it owns and what happens to it at export. `note` is user-facing
|
|
32
|
+
// One entry of a state manifest: the path it owns and what happens to it at export. `note` is user-facing,
|
|
33
33
|
// the import report prints it beside a skipped entry, so it is written as an instruction to the owner ("log in
|
|
34
34
|
// again"), not as a rationale for the next reader. Rationale belongs in a comment on the entry.
|
|
35
35
|
export interface StateFile {
|
|
@@ -42,7 +42,7 @@ export interface StateFile {
|
|
|
42
42
|
*
|
|
43
43
|
* Manifests may deliberately nest when one subtree has a different lifecycle. First-match resolution would
|
|
44
44
|
* classify the child by whichever entry happened to be listed first, so the specific entry has to win over the
|
|
45
|
-
* general one by construction rather than by list order
|
|
45
|
+
* general one by construction rather than by list order, the same reason a .gitignore consults its deepest
|
|
46
46
|
* matcher first.
|
|
47
47
|
*
|
|
48
48
|
* Undefined for a path no entry claims. Callers decide what that means: workspace content (including an
|
package/src/terminal-protocol.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
// The /system/terminal WebSocket wire protocol
|
|
1
|
+
// The /system/terminal WebSocket wire protocol. JSON text frames both ways (xterm speaks strings), shared by
|
|
2
2
|
// the daemon's terminal route and the browser's terminal session so the two can't drift. Plain types, not zod
|
|
3
3
|
// schemas: `data` is the per-pty-chunk hot path between two mutually authenticated endpoints, and no oRPC
|
|
4
4
|
// eventIterator validates these frames (which is why events.ts uses schemas).
|
|
5
5
|
|
|
6
6
|
// `ping` is the client's 30s keepalive against tunnel idle-reaping; the server answers with `pong`, so a
|
|
7
|
-
// healthy idle connection always sees a frame within the ping interval
|
|
7
|
+
// healthy idle connection always sees a frame within the ping interval, silence beyond it means half-open.
|
|
8
8
|
export type TerminalClientMessage =
|
|
9
9
|
| { readonly type: "input"; readonly data: string }
|
|
10
10
|
| { readonly type: "resize"; readonly cols: number; readonly rows: number }
|
|
11
11
|
| { readonly type: "ping" };
|
|
12
12
|
|
|
13
13
|
// `data` is raw pty output; `exit` fires when the tmux client ends (shell exited, or an attach-only session
|
|
14
|
-
// doesn't exist) and is terminal
|
|
14
|
+
// doesn't exist) and is terminal, the client never reconnects after it.
|
|
15
15
|
export type TerminalServerMessage =
|
|
16
16
|
{ readonly type: "data"; readonly data: string } | { readonly type: "exit"; readonly code: number } | { readonly type: "pong" };
|
package/src/title.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/* HOW A CONVERSATION NAMES ITSELF from its opening prompt
|
|
1
|
+
/* HOW A CONVERSATION NAMES ITSELF from its opening prompt, the title a chat tab, a fleet card and the agent
|
|
2
2
|
* detail header wear from the first keystroke of a turn until something better arrives.
|
|
3
3
|
*
|
|
4
4
|
* The naive rule (collapse whitespace, cut at N) reads the prompt as if it were a headline, and opening
|
|
5
5
|
* prompts are not headlines. They are pasted stack traces, links dropped in with two words of context,
|
|
6
6
|
* politeness that eats half the budget before the ask begins, and multi-line asks whose first line is throat-
|
|
7
|
-
* clearing. Cutting those at 40 characters yields `Error: ENOENT: no such file or directo
|
|
7
|
+
* clearing. Cutting those at 40 characters yields `Error: ENOENT: no such file or directo…`, a title that
|
|
8
8
|
* names the paste rather than the work, and that reads identically for every one of the day's five pastes.
|
|
9
9
|
*
|
|
10
10
|
* So the prompt is read as prose FIRST and cut second: quoted material and fenced code are not the ask, the
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
* that still says something, cut on a sentence or a word rather than mid-syllable.
|
|
13
13
|
*
|
|
14
14
|
* Both sides derive: the browser names a conversation the instant it sends (conversation.ts), and the daemon
|
|
15
|
-
* names a turn that arrived without one
|
|
15
|
+
* names a turn that arrived without one, an automation, a Discord message, a webchat visitor (agents-
|
|
16
16
|
* registry.ts). One rule, because two would let the same prompt open under two different names depending on
|
|
17
17
|
* where it entered. Nothing here calls a model: the title has to exist before the first frame comes back.
|
|
18
18
|
*
|
|
19
|
-
* Which is exactly the ceiling on it. Cutting is not naming
|
|
19
|
+
* Which is exactly the ceiling on it. Cutting is not naming, what comes out is the user's own sentence,
|
|
20
20
|
* shortened, and a column of those is scannable only where the users' sentences happened to differ early. The
|
|
21
21
|
* name a conversation ends up WEARING is written a second or two later by a model that reads the same prompt
|
|
22
22
|
* and answers in the fleet board's own shape (the daemon's title-namer.ts). This is the title that holds the
|
|
23
23
|
* tab until that arrives, and the one it keeps if nothing is connected to write a better one. */
|
|
24
24
|
|
|
25
|
-
// The registry's title budget (agents-registry MAX_TITLE_LENGTH, the rename input's maxlength)
|
|
25
|
+
// The registry's title budget (agents-registry MAX_TITLE_LENGTH, the rename input's maxlength), the widest
|
|
26
26
|
// any surface stores. Every surface truncates in CSS to its own width, so the clamp here is a storage cap,
|
|
27
27
|
// not a display rule: cutting shorter than the widest surface (a full-width fleet card) would throw away
|
|
28
28
|
// signal that surface has room to show, and no narrow surface is hurt because its CSS cuts first anyway.
|
|
@@ -35,7 +35,7 @@ const MIN_SENTENCE = 12;
|
|
|
35
35
|
const MIN_WORD_CUT = MAX_LENGTH * 0.6;
|
|
36
36
|
|
|
37
37
|
// Markdown's two fence syntaxes, and the quote marker. Both open and close on the same shape, so a fence line
|
|
38
|
-
// toggles rather than matching a pair
|
|
38
|
+
// toggles rather than matching a pair, an UNTERMINATED fence (a paste the user never closed) then swallows
|
|
39
39
|
// the rest of the prompt, which is what it visually does too.
|
|
40
40
|
const FENCE = /^\s*(?:```|~~~)/;
|
|
41
41
|
const QUOTE = /^\s*>/;
|
|
@@ -52,11 +52,11 @@ const OPENERS: readonly RegExp[] = [
|
|
|
52
52
|
/^(?:quick\s+(?:one|question|q)|one\s+more\s+thing)\b[\s,.:;!—–-]*/i,
|
|
53
53
|
];
|
|
54
54
|
|
|
55
|
-
// A sentence ends on terminal punctuation that is actually followed by a break
|
|
55
|
+
// A sentence ends on terminal punctuation that is actually followed by a break, which is what holds `v1.2`
|
|
56
56
|
// and `foo.ts` together while still ending `Why is it red?`.
|
|
57
57
|
const SENTENCE_END = /[.!?](?=\s|$)/;
|
|
58
58
|
|
|
59
|
-
/* A report of work already done
|
|
59
|
+
/* A report of work already done, `We have recently added…`, `I've just landed…`, `we were refactoring…`. The
|
|
60
60
|
* one first-sentence shape that is reliably CONTEXT rather than the ask: prompts that open on it set the scene
|
|
61
61
|
* for an instruction further down, and titling from the scene names every such conversation after last week's
|
|
62
62
|
* work instead of this one's.
|
|
@@ -69,7 +69,7 @@ const NARRATION =
|
|
|
69
69
|
/^(?:(?:recently|previously|earlier|today|yesterday|lately)[\s,]+)?(?:we|i)(?:'ve|'d|'m|'re)?(?:\s+(?:have|had|am|are|was|were|been|just|recently|already|earlier|previously|also|finally|now|currently|still))*\s+(?!(?:need|feed|speed|shed|heed|breed|bleed|embed|proceed|exceed|succeed)\b)(?:\w+ed|\w{3,}ing|built|wrote|rewrote|made|found|saw|thought|began|got|kept|put|set|sent|split|ran|did|redid|went|came|gave|held|hit|cut|let|read|understood|broke|chose|became|brought|spent|meant|lost|forgot|rebuilt|told|taught|stood|drew|grew|knew|threw|took|left|felt|hid)\b/i;
|
|
70
70
|
|
|
71
71
|
// The ask read as an instruction: a sentence leading with a bare verb of the kind coding asks lead with. A
|
|
72
|
-
// curated list rather than grammar
|
|
72
|
+
// curated list rather than grammar. English hands out noun/verb ambiguity too freely for anything cleverer.
|
|
73
73
|
const IMPERATIVE =
|
|
74
74
|
/^(?:add|fix|make|implement|create|build|write|rewrite|refactor|rename|remove|delete|drop|update|change|convert|migrate|move|extract|split|merge|rebase|revert|restore|wire|connect|integrate|support|handle|improve|clean|simplify|redesign|rework|rethink|investigate|analyze|analyse|debug|find|figure|check|verify|test|run|try|document|describe|explain|propose|design|draft|prepare|stop|prevent|ensure|allow|enable|disable|introduce|replace|swap|optimize|optimise|reduce|bump|upgrade|deploy|ship|release|adjust|tweak|polish|finish|complete|continue|extend|unify|dedupe|deduplicate|cache|persist|expose|hide|show|render|port|automate|wrap|inline|audit|review|profile|measure|instrument|validate|parse|generate|turn|look|think|come|consider|help|start|use|keep|avoid|teach|harden|tighten|localize|localise|translate)\b/i;
|
|
75
75
|
|
|
@@ -102,11 +102,11 @@ const proseLines = (prompt: string): string[] => {
|
|
|
102
102
|
|
|
103
103
|
/* Peel the openers off a line, but only keep the peeling when something with shape is left underneath.
|
|
104
104
|
*
|
|
105
|
-
* Three outcomes, and the difference between them is the whole point. A remnant of SEVERAL words is the ask
|
|
105
|
+
* Three outcomes, and the difference between them is the whole point. A remnant of SEVERAL words is the ask,
|
|
106
106
|
* `Can you please fix the auth tests?` was carrying `fix the auth tests?`. A remnant of NOTHING was pure
|
|
107
107
|
* greeting (`Hey, quick one —`), and returning empty is how the caller learns to read the next line instead.
|
|
108
|
-
* A remnant of ONE word means the opener was
|
|
109
|
-
* and `So what?` is not one about `what
|
|
108
|
+
* A remnant of ONE word means the opener was meaningful. `Hi there` is not a conversation about `there`,
|
|
109
|
+
* and `So what?` is not one about `what?`, so the line stands as written. */
|
|
110
110
|
const withoutOpener = (line: string): string => {
|
|
111
111
|
let text = line;
|
|
112
112
|
for (let pass = 0; pass < OPENERS.length; pass++) {
|
|
@@ -141,7 +141,7 @@ const shortUrl = (token: string): string => {
|
|
|
141
141
|
return /^\d+$/.test(last) && previous !== undefined ? `${previous}/${last}` : last;
|
|
142
142
|
};
|
|
143
143
|
|
|
144
|
-
// A path is worth its basename once it is deep enough that the lead is scaffolding
|
|
144
|
+
// A path is worth its basename once it is deep enough that the lead is scaffolding, `src/foo.ts` already
|
|
145
145
|
// reads as a place, `_editor/web/src/composables/chat/conversation.ts` reads as a wall.
|
|
146
146
|
const shortPath = (token: string): string => {
|
|
147
147
|
const segments = token.replace(/^@/, "").split("/");
|
|
@@ -149,7 +149,7 @@ const shortPath = (token: string): string => {
|
|
|
149
149
|
};
|
|
150
150
|
|
|
151
151
|
// Trailing punctuation belongs to the sentence, not to the reference inside it, so it is set aside across the
|
|
152
|
-
// shortening and put back
|
|
152
|
+
// shortening and put back, `see _editor/web/src/foo.ts,` collapses to `see foo.ts,`.
|
|
153
153
|
const shortReference = (token: string): string => {
|
|
154
154
|
const match = /^(.*?)([\s,.;:!?)\]]*)$/s.exec(token);
|
|
155
155
|
const core = match?.[1] ?? token;
|
|
@@ -175,7 +175,7 @@ const firstSentence = (text: string): string => {
|
|
|
175
175
|
return terminator === "." ? text.slice(0, end) : text.slice(0, end + 1);
|
|
176
176
|
};
|
|
177
177
|
|
|
178
|
-
// Every sentence in turn, terminators handled as in firstSentence
|
|
178
|
+
// Every sentence in turn, terminators handled as in firstSentence, the walk the ask-scan below reads. No
|
|
179
179
|
// abbreviation guard here: a fragment split off at an `e.g.` never leads with an imperative, so it simply
|
|
180
180
|
// loses the scan rather than corrupting it.
|
|
181
181
|
const sentencesOf = (text: string): string[] => {
|
|
@@ -194,11 +194,11 @@ const sentencesOf = (text: string): string[] => {
|
|
|
194
194
|
};
|
|
195
195
|
|
|
196
196
|
/* THE ELLIPSIS IS PART OF THE BUDGET, not an addition to it. MAX_LENGTH is a STORAGE cap the wire contract
|
|
197
|
-
* enforces (AgentTurnSchema.title), so a title one character over it is not a wide tab
|
|
197
|
+
* enforces (AgentTurnSchema.title), so a title one character over it is not a wide tab, it is a 400 on the
|
|
198
198
|
* turn that carried it. Cutting to MAX_LENGTH and appending afterwards was exactly that off-by-one, and it
|
|
199
199
|
* hid for as long as it did because the word-boundary backoff almost always absorbed it: an ordinary sentence
|
|
200
200
|
* has a space late in the window, so `kept` came back short and the ellipsis fit. What has no such space is a
|
|
201
|
-
* long unbroken token straddling the cut
|
|
201
|
+
* long unbroken token straddling the cut, a SCREAMING_SNAKE env var, a hash, a base64 blob, and there the
|
|
202
202
|
* fallback kept the full-width slice and made it one character too long. The conversation then wedged: the
|
|
203
203
|
* browser writes the derived title into its state before it sends, so every later turn re-sent the same
|
|
204
204
|
* rejected name. */
|
|
@@ -225,7 +225,7 @@ const capitalized = (text: string): string => {
|
|
|
225
225
|
/* Split a plan's markdown into its leading heading and the remaining body.
|
|
226
226
|
*
|
|
227
227
|
* The heading is the one place in a turn where an agent writes a name for the WHOLE job rather than for a step
|
|
228
|
-
|
|
228
|
+
*, a task checklist describes `Read the failing test`, `Fix the assertion`, `Run the suite`, none of which is
|
|
229
229
|
* what the conversation is about, whereas a plan opens `## Fix the flaky auth tests`. That makes it the only
|
|
230
230
|
* authored line in the stream worth promoting to a title, which is why this lives here rather than next to the
|
|
231
231
|
* plan card that also renders it: the browser titles its own tabs from it and the daemon titles fleet cards. */
|
|
@@ -248,7 +248,7 @@ export const deriveTitle = (prompt: string): string => {
|
|
|
248
248
|
const lines = prose.map(withoutOpener).filter((line) => /\p{L}/u.test(line));
|
|
249
249
|
const primary = lines[0];
|
|
250
250
|
if (primary !== undefined) {
|
|
251
|
-
/* A first sentence that reports work already done is the scene, not the ask
|
|
251
|
+
/* A first sentence that reports work already done is the scene, not the ask, `We have recently added
|
|
252
252
|
* iq map. Now let's expose health.` is a conversation about exposing health. Skipped only for an
|
|
253
253
|
* unmistakable ask further in (an instruction or an outright question); anything hazier and the
|
|
254
254
|
* narration stands, exactly as every non-narrative opening always does. */
|
|
@@ -261,7 +261,7 @@ export const deriveTitle = (prompt: string): string => {
|
|
|
261
261
|
: undefined;
|
|
262
262
|
return capitalized(clamped(firstSentence(collapseReferences(ask ?? primary))));
|
|
263
263
|
}
|
|
264
|
-
// Everything the user sent was code, a quote, or a greeting
|
|
264
|
+
// Everything the user sent was code, a quote, or a greeting, title it as what it is rather than blank.
|
|
265
265
|
const fallback = prose[0] ?? collapse(prompt.replaceAll(/^\s*(?:```|~~~).*$/gm, ""));
|
|
266
266
|
return clamped(collapseReferences(fallback === "" ? collapse(prompt) : fallback));
|
|
267
267
|
};
|
package/src/tunnel-ids.ts
CHANGED
|
@@ -22,33 +22,33 @@ export const hostSshIdFromToken = (connectToken: string, hostName: string): stri
|
|
|
22
22
|
* and it is the hard cap on preview DNS records a sandbox can ever cost the shared intentic zone. */
|
|
23
23
|
export const PORT_SLOT_COUNT = 8;
|
|
24
24
|
|
|
25
|
-
/* THE PORT-FORWARD SLOT LABELS
|
|
25
|
+
/* THE PORT-FORWARD SLOT LABELS, the `port-<slot>` half of `port-<slot>-<sandboxId>.<zone>`.
|
|
26
26
|
*
|
|
27
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
|
|
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
29
|
* of every preview link they have ever shared. So anyone who had seen ONE preview link could poll eight fixed
|
|
30
30
|
* names forever and catch whatever the owner forwarded, at any point in the future. The Ports view says a
|
|
31
31
|
* forwarded port is public, and it is; what it could not say was that "public" meant eight guessable URLs.
|
|
32
32
|
*
|
|
33
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
|
|
34
|
+
* records (the reason slots exist at all, the intentic-provided zone mints per label, and dev servers churn
|
|
35
35
|
* ephemeral ports far faster than DNS should), still stable across restarts so a slot's record stays warm, and
|
|
36
36
|
* still derivable with no coordination by every party that already holds the token: the daemon that forwards,
|
|
37
37
|
* and the platform that mints the DNS. A party without the token has no business predicting these names.
|
|
38
38
|
*
|
|
39
|
-
* The browser is deliberately NOT one of those parties
|
|
40
|
-
* off the daemon's response
|
|
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
41
|
* the digest it shares with sandboxIdFromToken. */
|
|
42
42
|
export const portSlotsFromToken = (connectToken: string): readonly string[] =>
|
|
43
43
|
Array.from({ length: PORT_SLOT_COUNT }, (_, index) => sha256Hex(`${connectToken}:port:${index}`).slice(0, 12));
|
|
44
44
|
|
|
45
|
-
/* THE OUTBOX SLOT
|
|
45
|
+
/* THE OUTBOX SLOT, the `public-<slot>` half of `public-<slot>-<sandboxId>.<zone>`, where the daemon serves the
|
|
46
46
|
* workspace's `public/` directory (PUBLIC_DIR in @intentic/workspace-ignore).
|
|
47
47
|
*
|
|
48
48
|
* Salted for the reason above, and it matters more here than it does for ports: a forwarded port is a live
|
|
49
49
|
* server the owner started minutes ago, whereas a published file sits there. One record per sandbox, stable
|
|
50
50
|
* across restarts so a link stays good for as long as the file does, and derivable only by parties holding the
|
|
51
|
-
* token
|
|
51
|
+
* token, the daemon that serves and the platform that mints. Not the browser: it reads the URL off the /public
|
|
52
52
|
* response, exactly as it reads previewUrl off /ports.
|
|
53
53
|
*
|
|
54
54
|
* The unguessable hostname is also what carries the security story, because the files under it have no auth in
|
package/src/versions.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* COMPARING THE VERSIONS THIS SYSTEM STAMPS ON WHAT IT SHIPS
|
|
1
|
+
/* COMPARING THE VERSIONS THIS SYSTEM STAMPS ON WHAT IT SHIPS, the daemon, the sandbox image, and the two agents
|
|
2
2
|
* that run on a user's own computer. One release stamps all of them to the SAME version, so "is this one behind
|
|
3
3
|
* that one" is one question with one answer, and it lives here because both ends ask it: the daemon compares its
|
|
4
4
|
* own build against the latest published release, and the browser compares a computer's agent against the same.
|
|
@@ -33,14 +33,14 @@ export const DEV_VERSION = `0.0.0`;
|
|
|
33
33
|
/* WHETHER TO TELL SOMEBODY THEIR BUILD IS OLD. Deliberately false in every uncertain case, and each one is a
|
|
34
34
|
* different kind of not-knowing:
|
|
35
35
|
*
|
|
36
|
-
* • no installed version
|
|
37
|
-
* • no latest version
|
|
38
|
-
* • installed is the dev sentinel
|
|
36
|
+
* • no installed version, the thing does not report one, so there is nothing to be behind.
|
|
37
|
+
* • no latest version, this sandbox has not reached the registry (or is a dev build and never will).
|
|
38
|
+
* • installed is the dev sentinel, see above; a nag that cannot be satisfied is worse than silence.
|
|
39
39
|
*
|
|
40
40
|
* A version that is not dotted-numeric is compared by its numeric prefix, which is both useful and safe in the
|
|
41
41
|
* only direction that matters: a segment that will not parse compares as neither greater nor less, so it stops
|
|
42
42
|
* the comparison at "not newer". A malformed INSTALLED version can therefore only ever withhold a nag, never
|
|
43
|
-
* invent one
|
|
43
|
+
* invent one, and `latest` comes from the registry, so it is well-formed by construction.
|
|
44
44
|
*
|
|
45
45
|
* The asymmetry is the point. Saying "you are out of date" wrongly sends someone to reinstall a working agent;
|
|
46
46
|
* saying nothing wrongly leaves them where they already were. */
|
package/src/workflow-faults.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* WHY A WORKFLOW GRAPH IS NOT RUNNABLE
|
|
1
|
+
/* WHY A WORKFLOW GRAPH IS NOT RUNNABLE, the rules that `WorkflowSchema` cannot state, because each of them is
|
|
2
2
|
* about the graph rather than about one field.
|
|
3
3
|
*
|
|
4
4
|
* A rule per function, so the reason a graph is refused reads as one sentence in one place. They are
|
|
@@ -49,24 +49,24 @@ const stepFaults = (step: WorkflowStep, ids: ReadonlySet<string>): string[] => {
|
|
|
49
49
|
),
|
|
50
50
|
);
|
|
51
51
|
}
|
|
52
|
-
/* A STEP THAT DECLARES NOTHING IS NOT A FAULT
|
|
52
|
+
/* A STEP THAT DECLARES NOTHING IS NOT A FAULT, it is the ordinary step, and this used to refuse it.
|
|
53
53
|
*
|
|
54
54
|
* The rule was borrowed from loops (`loopCanConverge`), where it is right: a LOOP is started to repeat
|
|
55
55
|
* until something is true, so one with nothing to produce and nothing to check has no reason to run twice
|
|
56
56
|
* and the dialog is correct to grey out its button. A STEP is not started to repeat. It is one agent
|
|
57
|
-
* session with a job, and the job being done is the turn ending
|
|
57
|
+
* session with a job, and the job being done is the turn ending, which is exactly what the loop machinery
|
|
58
58
|
* already does with it (loop-stop's `readDocument` answers `done` for a `none` output, so iteration 1 is
|
|
59
59
|
* the only iteration).
|
|
60
60
|
*
|
|
61
61
|
* Keeping the rule here forced every step to declare an output or a check before the graph would save, and
|
|
62
|
-
* the cheapest way to satisfy it was a `claim
|
|
62
|
+
* the cheapest way to satisfy it was a `claim`, which buys a verdict file nobody reads, a page of contract
|
|
63
63
|
* in the prompt, and a way for a step that did the work to fail for not having described it.
|
|
64
64
|
*/
|
|
65
65
|
return faults;
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
/* Two steps continuing the SAME session is the one graph that is legal on paper and broken in practice: both
|
|
69
|
-
* would run on one conversation, in parallel, against one worktree and one turn mutex
|
|
69
|
+
* would run on one conversation, in parallel, against one worktree and one turn mutex, so they would serialize
|
|
70
70
|
* on a lock neither knows about and the second would inherit a session the first had moved on. A predecessor
|
|
71
71
|
* can be continued once; anything else that needs its result takes it as a handover. */
|
|
72
72
|
const sharedContinuationFaults = (steps: readonly WorkflowStep[]): string[] => {
|
|
@@ -110,8 +110,8 @@ const cycleFaults = (steps: readonly WorkflowStep[]): string[] => {
|
|
|
110
110
|
return faults;
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
-
/* A GATE THAT CANNOT BE ANSWERED. Every rule here is about the gate against the GRAPH
|
|
114
|
-
* them can live in the schema
|
|
113
|
+
/* A GATE THAT CANNOT BE ANSWERED. Every rule here is about the gate against the GRAPH, which is why none of
|
|
114
|
+
* them can live in the schema, and all of them fail the same expensive way if unchecked: the run spends its
|
|
115
115
|
* whole fan-out of sessions and then answers `blocked`, on every commit, for a reason nobody sees until they
|
|
116
116
|
* go reading the daemon's log.
|
|
117
117
|
*
|
|
@@ -138,7 +138,7 @@ const gateFaults = (workflow: Pick<Workflow, "steps" | "gate">): string[] => {
|
|
|
138
138
|
if (field.type === "string[]") {
|
|
139
139
|
return [`The gate reads "${gate.field}", which is a list — a release decision has to be one value.`];
|
|
140
140
|
}
|
|
141
|
-
// The schema refuses this on save (pass is min(1)), but the designer edits drafts the schema never sees
|
|
141
|
+
// The schema refuses this on save (pass is min(1)), but the designer edits drafts the schema never sees,
|
|
142
142
|
// and an empty allowlist is a gate no run could ever answer "pass", which deserves a sentence, not a save error.
|
|
143
143
|
if (gate.pass.length === 0) {
|
|
144
144
|
return [`The gate names no passing values, so no run could ever ship.`];
|
|
@@ -163,13 +163,13 @@ export const workflowFaults = (workflow: Pick<Workflow, "steps" | "gate">): stri
|
|
|
163
163
|
];
|
|
164
164
|
};
|
|
165
165
|
|
|
166
|
-
/* WHAT ONLY A RUN CAN BE WRONG ABOUT
|
|
166
|
+
/* WHAT ONLY A RUN CAN BE WRONG ABOUT, kept apart from the rules above because it is not about the graph, and
|
|
167
167
|
* the graph is what gets SAVED. A design whose steps take their goal and instruction from the request is a
|
|
168
168
|
* perfectly good design; it is only unrunnable on the particular run that forgot to bring one.
|
|
169
169
|
*
|
|
170
170
|
* Which is why this cannot be a save-time rule and must not become one: refusing to save such a workflow would
|
|
171
|
-
* outlaw the entire point of a workflow being a SHAPE. The check belongs at the two doors that start runs
|
|
172
|
-
* run route and the gate's webhook
|
|
171
|
+
* outlaw the entire point of a workflow being a SHAPE. The check belongs at the two doors that start runs, the
|
|
172
|
+
* run route and the gate's webhook, and it has to be there rather than left to fail later, because "later"
|
|
173
173
|
* means every session in the fan-out has already been paid for before anyone finds out the model was handed an
|
|
174
174
|
* empty instruction.
|
|
175
175
|
*/
|