@intentic/extension-api 1.310.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 +32 -189
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +16 -4
- package/dist/background.js.map +1 -1
- package/package.json +2 -2
- package/src/background.ts +24 -6
package/README.md
CHANGED
|
@@ -1,196 +1,39 @@
|
|
|
1
|
-
#
|
|
1
|
+
# extension-api
|
|
2
2
|
|
|
3
|
-
The
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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)
|
|
185
|
-
- [src/
|
|
186
|
-
- [src/
|
|
187
|
-
|
|
188
|
-
- [src/
|
|
189
|
-
- [src/
|
|
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.
|
package/dist/background.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/background.js
CHANGED
|
@@ -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
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
128
|
+
const seen = await readToWrite();
|
|
117
129
|
const wanted = next(seen);
|
|
118
130
|
if (sameEntries(seen, wanted)) {
|
|
119
131
|
return true;
|
package/dist/background.js.map
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
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)) {
|