@intentic/extension-api 1.309.0 → 1.311.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 CHANGED
@@ -1,196 +1,39 @@
1
- # @intentic/extension-api
1
+ # extension-api
2
2
 
3
- The one SDK an extension programs against: the extension-author contract for the intentic app.
3
+ The versioned public API an intentic extension compiles against: `IntenticApi` for its browser half, `ExtensionServerApi` for its backend half, and the helpers both halves share.
4
4
 
5
- One of the packages an extension may depend on, with `@intentic/extension-manifest`,
6
- `@intentic/extension-ui` and `@intentic/sandbox-contract`. Published to npm; it must stay free of app
7
- internals. See the extension system in [ARCHITECTURE.md](../../ARCHITECTURE.md) for how the host loads and
8
- gates extensions.
9
-
10
- It **does** name `@intentic/sandbox-contract` types, and that is deliberate: `api.sandbox.rpc` (and the backend's
11
- `api.daemon.rpc`) is the daemon's own contract as a typed client, which is the whole reason an extension no longer has to build a URL to reach
12
- it. The dependency is type-only, so nothing of the contract lands in an extension's runtime. This used to be
13
- forbidden: the contract imported the manifest schema from here, so depending back would have closed a cycle.
14
- `@intentic/extension-manifest` exists to break exactly that, and its README has the reasoning.
15
-
16
- ## What's here
17
-
18
- - **[api.ts](src/api.ts)**: `IntenticApi`, the host surface delivered to `activate(api, context)`. There is
19
- no ambient global; everything an extension registers is a `Disposable` pushed onto
20
- `context.subscriptions`, so deactivation unwinds it. `api.sandbox.rpc` is the typed daemon client, gated by
21
- the manifest's `permissions.sandbox` allowlist exactly as the older `request`/`json` doors are.
22
- - **The manifest schema lives in [@intentic/extension-manifest](../extension-manifest)**, not here: it is what
23
- an extension *declares*, and the daemon needs it without needing any of this package. The manifest is the
24
- **approval + gating surface**: the install dialog shows exactly the declared contribution points, and the host
25
- refuses any runtime registration (view, command, viewer, setting, process…) the approved manifest never
26
- declared. Contribution points: `views`, `files`, `viewers`, `documents`, `commands`, `settings`,
27
- `processes`, `agent`, `environment`, `capabilities`, `listener`, `automationTemplates`, `bin`, plus the
28
- `permissions.sandbox` route allowlist. That list is not prose to be kept in sync by hand:
29
- `surface-guard.test.ts` reads it back out of this file and fails when it stops matching the schema.
30
- Every one of those points carries its own description, generated out to
31
- [an authoring schema](https://intentic.dev/intentic-extension.schema.json); point a manifest's `$schema` at
32
- it and an editor completes the fields, explains what each does, and marks a key nothing declares.
33
- A `listener` owns both halves of its public vocabulary: labelled event types for daemon validation and the
34
- source/filter/starter wording a generic automation editor renders. Installing a listener therefore adds a
35
- configurable automation source without an app release or a second provider table.
36
- `automationTemplates` is the other half of that bargain: the starting points for a pack's own service:
37
- trigger, prompt, guard, setup instructions: declared by whoever knows the service rather than written into
38
- the automations surface. Both fold into one catalogue the daemon serves (`GET /automations/catalog`), which
39
- is also what `POST /automations` validates against, so the editor cannot offer a trigger the daemon refuses.
40
- Identity is derived, never declared: `extensionIdOf(manifest) = ${publisher}.${name}`.
41
- - **[facts.ts](src/facts.ts)**: the stable **detection** vocabulary (`RepoFacts`, `CapabilityFacts`) a
42
- view's `detect()` reads to decide when to activate. This is *not* the data plane.
43
- - **[server.ts](src/server.ts)**: `ExtensionServerApi`, the BACKEND half's surface. A manifest `server`
44
- bundle exports `activateServer(api, context)` and runs in the daemon's backend host (one separate
45
- supervised process shared by every enabled backend); `api.routes.mount` serves the extension's own
46
- `/x/<id>/…` namespace, `api.daemon.rpc` reaches the daemon's routes under the manifest's
47
- `permissions.daemon` allowlist, and workspace files are plain `node:fs` under `api.workspaceRoot`: full
48
- trust, so paths rather than a file service. The extension's own namespace needs no `permissions.sandbox`
49
- entry on the UI side: its backend is its own.
50
-
51
- Three surfaces, at three different grains, and the grain is what picks one. A **view** activates per *repo*
52
- off the facts (`rail`, `directory`, `sandbox`). A **viewer** takes over a *file extension*. A **document**
53
- answers per *directory*: `detect(path)` marks the rows it can explain in the Workspace tree, and the host
54
- opens the provider's component as a tab beside the code. A monorepo is one repo with fifty-five documented
55
- packages, which is exactly the case a per-repo `detect()` cannot express. An offer that is EVIDENCE about the
56
- directory rather than an affordance every directory of its kind has says so (`evidence: true`), and the tree
57
- keeps its icon on the row instead of revealing it on hover: the difference between a reader seeing which
58
- packages have a page and a reader having to go looking for one.
59
- - **[scope.ts](src/scope.ts)**, `sandboxRef` (with `sandboxShallowRef` and `sandboxValue`) and
60
- `sandboxScopeGuard`: how an extension keeps state that belongs to ONE sandbox. See "Where state lives" below;
61
- this is the rule most easily got wrong, because getting it wrong looks fine until somebody switches sandbox.
62
- - **[background.ts](src/background.ts)**, `sandboxPoll` and `sandboxLedger`: the work an extension does while
63
- none of it is on screen. A tile that badges has to be filled by something, and what has already been seen has
64
- to be written down somewhere; both were hand-written in six extensions before they were here.
65
- - **[stream.ts](src/stream.ts)**, **[version.ts](src/version.ts)**: SSE/ndjson helpers and the host API
66
- version (`engines.intentic` is checked against it before activation).
67
-
68
- Version 2 makes listener contributions self-describing (`events` + `automation`); version 1 listeners only
69
- declared bare event ids and cannot describe a generic editor. Version 2.1 adds the backend half: the
70
- manifest `server` bundle and `permissions.daemon`, additively: a 2.0 manifest is a 2.1 manifest that ships
71
- no backend.
72
-
73
- ## The data plane
74
-
75
- An extension talks to the daemon through `api.sandbox.rpc`, the daemon's own contract as a typed client: a call
76
- names a procedure (`api.sandbox.rpc.ci.runs()`), its input is checked at build time, and its answer arrives parsed
77
- by the procedure's output schema, so there is no path to spell and no parse to write. Auth is injected host-side;
78
- the bundle never sees a token. **Its reach is not unrestricted:** the host resolves each call to the method and
79
- path it will send and matches that against the manifest's `permissions.sandbox` allowlist, the same gate
80
- `request`/`json` pass through, and an undeclared route throws before anything is sent.
81
-
82
- `request(path)` stays for what the contract does not carry: bytes (`/workspace/raw`), uploads, an extension's own
83
- `/x/<id>/` backend. `json(path)` stays in the published surface for extensions built against it; every extension
84
- in this repository calls a contract route through `rpc` (the `contract-paths` check holds them to it).
85
-
86
- A backend reaches the daemon the same way, through `api.daemon.rpc`: the same typed client, built by the daemon's
87
- backend host and handed to `activateServer`, so a server bundle carries no client of its own. It presents the
88
- extension's minted grant, and the host refuses a call before sending it unless the manifest's `permissions.daemon`
89
- covers the method and path it resolves to: the verdict the daemon's grant reaches for the same call through
90
- `api.daemon.request`, which stays for bytes (`/workspace/raw`), as `json` does for backends built against it.
91
-
92
- The in-repo, compiled-together design means a wire change is a compiler error fixed atomically, so there is no
93
- separate "stable data API" to promote. `facts.ts` stays the stable surface only for *detection*.
94
-
95
- ## Where state lives
96
-
97
- Three tiers, and the tier decides what happens when the user points the browser at a **different sandbox**.
98
- Everything an extension holds is about one workspace, so a switch has to leave nothing of the last one behind.
99
-
100
- - **Cached reads**: `useQuery` in a view, or `api.sandbox.fetch(query)` from outside one. Key them with
101
- `api.sandbox.key(...)` and the switch is handled by construction: the key carries the active sandbox id, so
102
- the next box is a different cache entry. Use the *same* key for a view's query and for the badge poll that
103
- warms it, and the poll's answer becomes the view's first paint.
104
- - **State inside a mounted component**: an ordinary `ref` in a `.vue` file. Nothing to do; it dies with the
105
- component.
106
- - **Module state owned by `activate()`**: the badge counts, presence maps and poll results that must survive
107
- the view being unmounted, because a badge you only see after opening the view is pointless. Declare it with
108
- `sandboxRef(() => initial)` and the host empties it on every switch. There is no subscription to remember
109
- and no teardown to write; `dispose` is there for state that owns an object URL or anything else the garbage
110
- collector will not take back. A value replaced whole rather than edited in place (a map, an object with its
111
- own methods) goes in `sandboxShallowRef` instead, the same lifetime without the deep proxy.
112
-
113
- For anything asynchronous in that third tier, take a `sandboxScopeGuard()` **before** the await and ask it
114
- **after**: a poll issued against the last sandbox otherwise resolves a moment later and writes its answer
115
- into the new one, which is the same wrong badge with a harder repro. It matters twice over for a call that
116
- WRITES: acknowledging what a badge has shown, in the wrong workspace's tree, is bookkeeping no later poll
117
- corrects.
118
-
119
- ## Keeping a tile current while nothing is mounted
120
-
121
- Most of that third tier exists to feed a rail badge, so `sandboxPoll` covers the whole shape and you should not
122
- need `sandboxRef` directly for one:
123
-
124
- ```ts
125
- const { state: unseen, start } = sandboxPoll<readonly Finding[]>({
126
- host, // your hostSlot's accessor — nothing is bound until activate()
127
- everyMs: 10 * 60_000, // a backstop here: the file binding below is what actually refreshes this
128
- initial: () => [],
129
- read: async (api) => findings(await api.sandbox.fetch(query())),
130
- });
5
+ ```mermaid
6
+ flowchart LR
7
+ web["Editor<br/>extension host"] -- "activate(api, context)" --> view["Extension bundle<br/>views · commands"]
8
+ backend["Daemon<br/>backend host"] -- "activateServer(api)" --> server["Server bundle<br/>routes under /x/id"]
9
+ sdk(["extension-api<br/>types · helpers"]) -.-> view
10
+ sdk -.-> server
11
+ loader["Daemon loader"] -- "engines.intentic<br/>vs extensionApiVersion" --> sdk
131
12
  ```
132
13
 
133
- `start()` returns the `Disposable` to push onto `context.subscriptions`; `refresh()` reads off-cycle for the
134
- moments that should not wait out the interval. The five rules a hand-written version has to remember: never
135
- reject, skip an unreachable daemon, discard an answer that outlived its sandbox, keep the last good value on
136
- failure, stop the clock on disposal, are the poll's, not yours. Pass `immediate: false` if there is nothing
137
- worth asking until something else tells you what to ask about, and read `previous` in `read` if a round
138
- accumulates onto what you already hold rather than replacing it.
139
-
140
- **If the answer lives in a file, the write is the feed and the interval is only a backstop.** `start()`
141
- subscribes to your own `contributes.files` paths being written (`api.workspace.onDidChangeFiles`), so a
142
- declaration you already made is what moves the tile, within a frame of the file changing rather than at the next
143
- tick. Nothing to opt into: declare the paths your views derive from and the badge over them reacts; declare none
144
- and the poll is the timer it always was. Bursts coalesce, so a run writing a file per story costs one read, and a
145
- reconnect wakes it too, because a frame pushed while the stream was down is a frame nobody resends. Pick
146
- `everyMs` accordingly: minutes for a file-backed badge (it is covering a dropped event, not carrying the news),
147
- and the honest cadence of the source for anything behind somebody else's API, where nothing can push at all.
148
- All polls in a window share one background-read lane, so aligned extension clocks and reconnect wakes cannot
149
- turn into parallel request bursts. Repeated triggers coalesce while waiting; one that lands during the read gets
150
- one trailing pass, so serialization does not trade load for a missed update.
151
-
152
- What the tile SAYS stays yours: `badge()` is the judgement each surface exists to make, and no two of them
153
- agree about tone or wording.
154
-
155
- It says it on two channels, and which one you reach for is the only rule the host enforces. `count`/`mark`
156
- is what the reader is OWED, and it wears the chip: a number, a tone, something to go and do. `running` is
157
- what is HAPPENING, a sentence like `2 running`, and it draws as a turning mark in the tile's own corner with
158
- no plate and no tone. Fill both when both are true — a red branch with its fix already re-running is the
159
- ordinary case, not the exotic one — and the tile says so without either claim evicting the other. Either
160
- channel seats the tile: the rail has always shown live work, and a run in flight that had to fail before
161
- earning a tile would be invisible for exactly as long as watching it was worth anything. A running mark needs
162
- no read marker and no ledger, which is the other half of why it is a separate channel: it goes out by itself
163
- when the work ends.
164
-
165
- `sandboxLedger(host, path)` is the other half: the JSON file recording what the owner has already seen, as
166
- `key → mark`, where the mark is what makes an entry stale. Compare marks (a chore's evidence digest, a story's
167
- verdict) and the same key with new evidence is news again; ignore them and it is a plain presence ledger. It
168
- reads a missing or mangled file as "nothing acknowledged", writes nothing when nothing moved, and holds the
169
- scope guard across its own read-then-write so an acknowledgement cannot land in the wrong workspace's tree.
170
-
171
- This is not advice. `sandboxScope.guard.test.ts` in the app walks each extension's UI entry through its own
172
- imports and refuses module-level `ref`/`shallowRef`/`reactive`, any reassignable module binding, and any
173
- repeating clock in what it reaches, because the failure it prevents was found in six extensions at once: a
174
- rail tile reading `21` under a workspace that had two.
175
-
176
- ## Authoring an extension
177
-
178
- `activate(api, context)` registers contributions and returns; `deactivate` is optional. A UI extension also
179
- ships a prebuilt single-file ESM `entry` bundle (built with `vue` and `@intentic/extension-api` external).
180
- The five UI extensions under [`_extensions/`](../../_extensions) are the working templates; start from one.
14
+ - There is no ambient global. The host hands `IntenticApi` to `activate(api, context)`, and everything registered
15
+ returns a `Disposable`. A manifest's `server` bundle gets `ExtensionServerApi` in a Node process shared by every
16
+ enabled extension, and reaches daemon routes only as far as `permissions.daemon` allows.
17
+ - This package is the one exception to the repo's no-legacy rule. It is published to npm, `extensionApiVersion` moves
18
+ with every surface change (additive is a minor), and a manifest's `engines.intentic` range is matched against it at
19
+ load, failing closed. The editor's `surface-guard.test.ts` fails when the surface moves without a new entry in
20
+ `src/surface.json`.
21
+ - Helpers every extension needs live here too: sandbox-scoped module state (`sandboxRef`, cleared on every sandbox
22
+ switch), background polling for rail badges, `sandboxLedger` (whose writes reject rather than overwrite a file they
23
+ could not read), SSE and ndjson stream reading, and the payload for
24
+ `api.workspace.openDiff`.
25
+ - Contribution points: `agent`, `automationTemplates`, `bin`, `capabilities`, `commands`, `documents`,
26
+ `environment`, `files`, `listener`, `processes`, `settings`, `viewers` and `views`, plus the manifest's top-level
27
+ fields, all read off `CONTRIBUTION_POINTS` in
28
+ [extension-manifest](../extension-manifest/src/points/index.ts).
29
+ - `./protocol` exports only the version and the engines matcher, for the daemon, which cannot load the Vue-dependent
30
+ barrel.
181
31
 
182
32
  ## Key files
183
33
 
184
- - [src/api.ts](src/api.ts): the handle an extension is given; the centre of this package.
185
- - [src/facts.ts](src/facts.ts): the public facts a view's `detect()` answers from.
186
- - [src/engines.ts](src/engines.ts): how `engines.intentic` is matched against the version below, for the host
187
- and the daemon alike.
188
- - [src/route.ts](src/route.ts): the query rules a view with internal navigation uses.
189
- - [src/scope.ts](src/scope.ts): module state that belongs to one sandbox, and the guard for work in flight
190
- across a switch.
191
- - [src/version.ts](src/version.ts) and [src/surface.json](src/surface.json): the protocol version, and what
192
- each version of it promised.
193
-
194
- The manifest schema and the `permissions.sandbox` matcher are **not here**: they moved to
195
- [@intentic/extension-manifest](../extension-manifest), which exists so the daemon can read a manifest without
196
- depending on the browser-facing API.
34
+ - [src/api.ts](src/api.ts) — `IntenticApi`, `ExtensionContext` and the module shape a bundle exports.
35
+ - [src/server.ts](src/server.ts) — `ExtensionServerApi`, the backend half.
36
+ - [src/version.ts](src/version.ts) — `extensionApiVersion` and what each version added.
37
+ - [src/surface.json](src/surface.json) — the recorded public surface, per version.
38
+ - [src/scope.ts](src/scope.ts) — module state that belongs to one sandbox.
39
+ - [src/background.ts](src/background.ts) — `sandboxPoll` and `sandboxLedger` for work done off screen.
@@ -1 +1 @@
1
- {"version":3,"file":"background.d.ts","sourceRoot":"","sources":["../src/background.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAOxD,MAAM,WAAW,WAAW,CAAC,CAAC;IAE1B,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IAEvB,KAAK,IAAI,UAAU,CAAC;IAEpB,OAAO,IAAI,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB,CAAC,CAAC;IAEjC,QAAQ,CAAC,IAAI,EAAE,MAAM,WAAW,CAAC;IAEjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAE1B,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAE7D,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAE7B,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC;CAC5C;AAqFD,eAAO,MAAM,WAAW,GAAI,CAAC,WAAW,kBAAkB,CAAC,CAAC,CAAC,KAAG,WAAW,CAAC,CAAC,CAsC5E,CAAC;AAIF,MAAM,WAAW,aAAa;IAE1B,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IAElD,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAElE,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACxE;AAKD,eAAO,MAAM,aAAa,SAAU,MAAM,WAAW,QAAQ,MAAM,KAAG,aA6BrE,CAAC"}
1
+ {"version":3,"file":"background.d.ts","sourceRoot":"","sources":["../src/background.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAOxD,MAAM,WAAW,WAAW,CAAC,CAAC;IAE1B,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IAEvB,KAAK,IAAI,UAAU,CAAC;IAEpB,OAAO,IAAI,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB,CAAC,CAAC;IAEjC,QAAQ,CAAC,IAAI,EAAE,MAAM,WAAW,CAAC;IAEjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAE1B,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAE7D,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAE7B,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC;CAC5C;AAqFD,eAAO,MAAM,WAAW,GAAI,CAAC,WAAW,kBAAkB,CAAC,CAAC,CAAC,KAAG,WAAW,CAAC,CAAC,CAsC5E,CAAC;AAKF,MAAM,WAAW,aAAa;IAE1B,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IAElD,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAElE,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACxE;AAKD,eAAO,MAAM,aAAa,SAAU,MAAM,WAAW,QAAQ,MAAM,KAAG,aA8CrE,CAAC"}
@@ -107,13 +107,25 @@ export const sandboxPoll = (options) => {
107
107
  };
108
108
  const sameEntries = (left, right) => Object.keys(left).length === Object.keys(right).length && Object.entries(left).every(([key, mark]) => right[key] === mark);
109
109
  export const sandboxLedger = (host, path) => {
110
- const read = async () => {
111
- const parsed = await host().workspace.readJson(path);
112
- return Object.fromEntries(Object.entries(parsed ?? {}).filter((entry) => typeof entry[1] === `string`));
110
+ const marksOf = (parsed) => typeof parsed === `object` && parsed !== null && !Array.isArray(parsed)
111
+ ? Object.fromEntries(Object.entries(parsed).filter((entry) => typeof entry[1] === `string`))
112
+ : {};
113
+ const read = async () => marksOf(await host().workspace.readJson(path));
114
+ const readToWrite = async () => {
115
+ const answer = await host().sandbox.rpc.workspace.file({ path });
116
+ if (!answer.present) {
117
+ return {};
118
+ }
119
+ try {
120
+ return marksOf(JSON.parse(answer.content));
121
+ }
122
+ catch {
123
+ return {};
124
+ }
113
125
  };
114
126
  const settle = async (next) => {
115
127
  const current = sandboxScopeGuard();
116
- const seen = await read();
128
+ const seen = await readToWrite();
117
129
  const wanted = next(seen);
118
130
  if (sameEntries(seen, wanted)) {
119
131
  return true;
@@ -1 +1 @@
1
- {"version":3,"file":"background.js","sourceRoot":"","sources":["../src/background.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"background.js","sourceRoot":"","sources":["../src/background.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentic/extension-api",
3
- "version": "1.309.0",
3
+ "version": "1.311.0",
4
4
  "description": "The versioned public API intentic extensions compile against, manifest schema, detection facts and the host API",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,7 +42,7 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@intentic/sandbox-contract": "1.309.0",
45
+ "@intentic/sandbox-contract": "1.311.0",
46
46
  "@orpc/contract": "1.14.13",
47
47
  "tslib": "2.8.1"
48
48
  },
package/src/background.ts CHANGED
@@ -154,7 +154,8 @@ export const sandboxPoll = <T>(options: SandboxPollOptions<T>): SandboxPoll<T> =
154
154
  };
155
155
 
156
156
  // Key → mark map of what the owner has already seen, stored as JSON under `.intentic`. Missing or unparseable reads as
157
- // nothing acknowledged; a write's `false` return means only that the sandbox scope moved mid-write.
157
+ // nothing acknowledged; a write's `false` return means only that the sandbox scope moved mid-write, and a write over a
158
+ // ledger that could not be read at all (refused, unreachable) rejects rather than dropping every acknowledgement in it.
158
159
  export interface SandboxLedger {
159
160
  // Everything acknowledged so far. Absent, unparseable or not-an-object all read as nothing.
160
161
  read(): Promise<Readonly<Record<string, string>>>;
@@ -168,17 +169,34 @@ const sameEntries = (left: Readonly<Record<string, string>>, right: Readonly<Rec
168
169
  Object.keys(left).length === Object.keys(right).length && Object.entries(left).every(([key, mark]) => right[key] === mark);
169
170
 
170
171
  export const sandboxLedger = (host: () => IntenticApi, path: string): SandboxLedger => {
171
- const read = async (): Promise<Readonly<Record<string, string>>> => {
172
- const parsed = await host().workspace.readJson<Record<string, unknown>>(path);
173
- // Non-string values are dropped, not coerced: a mark is always a string.
174
- return Object.fromEntries(Object.entries(parsed ?? {}).filter((entry): entry is [string, string] => typeof entry[1] === `string`));
172
+ // Non-string values are dropped, not coerced: a mark is always a string.
173
+ const marksOf = (parsed: unknown): Readonly<Record<string, string>> =>
174
+ typeof parsed === `object` && parsed !== null && !Array.isArray(parsed)
175
+ ? Object.fromEntries(Object.entries(parsed).filter((entry): entry is [string, string] => typeof entry[1] === `string`))
176
+ : {};
177
+
178
+ const read = async (): Promise<Readonly<Record<string, string>>> => marksOf(await host().workspace.readJson<Record<string, unknown>>(path));
179
+
180
+ // Through the contract's own read, which throws for a refused or unreachable read where `readJson` answers absent:
181
+ // a write computed from that absence would drop every acknowledgement the file really holds.
182
+ const readToWrite = async (): Promise<Readonly<Record<string, string>>> => {
183
+ const answer = await host().sandbox.rpc.workspace.file({ path });
184
+ if (!answer.present) {
185
+ return {};
186
+ }
187
+ try {
188
+ return marksOf(JSON.parse(answer.content));
189
+ } catch {
190
+ // silent-catch: an unparseable ledger holds nothing acknowledged, the contract `read` states.
191
+ return {};
192
+ }
175
193
  };
176
194
 
177
195
  // One writer for both mark and replace; the scope guard lives here, not at the call site, since a sandbox switch
178
196
  // mid-write would file the acknowledgement into the workspace the owner just left.
179
197
  const settle = async (next: (seen: Readonly<Record<string, string>>) => Readonly<Record<string, string>>): Promise<boolean> => {
180
198
  const current = sandboxScopeGuard();
181
- const seen = await read();
199
+ const seen = await readToWrite();
182
200
  const wanted = next(seen);
183
201
  // Already saying it: nothing to write, and the caller's fold stays correct.
184
202
  if (sameEntries(seen, wanted)) {