@plurnk/plurnk-web 0.0.0-stage → 1.1.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/LICENSE +21 -0
- package/README.md +337 -2
- package/SPEC.md +869 -0
- package/ai.plurnk/.env.defaults +38 -0
- package/ai.plurnk/assets.json +193 -0
- package/ai.plurnk/creations.js +65 -0
- package/ai.plurnk/diagram-compiler.js +15 -0
- package/ai.plurnk/diagrams.js +130 -0
- package/ai.plurnk/directory-addresses.js +35 -0
- package/ai.plurnk/dist/assets/Temml-CCjoacWt.woff2 +0 -0
- package/ai.plurnk/dist/assets/index-B4w6iDKw.css +1 -0
- package/ai.plurnk/dist/assets/index-B4w6iDKw.css.br +0 -0
- package/ai.plurnk/dist/assets/index-B4w6iDKw.css.gz +0 -0
- package/ai.plurnk/dist/assets/index-CGRGblLU.js +2 -0
- package/ai.plurnk/dist/assets/index-CGRGblLU.js.br +0 -0
- package/ai.plurnk/dist/assets/index-CGRGblLU.js.gz +0 -0
- package/ai.plurnk/dist/assets/math-renderer-CZ_XtOMq.css +1 -0
- package/ai.plurnk/dist/assets/math-renderer-CZ_XtOMq.css.br +0 -0
- package/ai.plurnk/dist/assets/math-renderer-CZ_XtOMq.css.gz +0 -0
- package/ai.plurnk/dist/assets/math-renderer-CnlgS6FH.js +96 -0
- package/ai.plurnk/dist/assets/math-renderer-CnlgS6FH.js.br +0 -0
- package/ai.plurnk/dist/assets/math-renderer-CnlgS6FH.js.gz +0 -0
- package/ai.plurnk/dist/assets/overview-DEhW9HUG.js +1 -0
- package/ai.plurnk/dist/assets/overview-DEhW9HUG.js.br +0 -0
- package/ai.plurnk/dist/assets/overview-DEhW9HUG.js.gz +0 -0
- package/ai.plurnk/dist/assets/preload-helper-BjS5LQBu.js +1 -0
- package/ai.plurnk/dist/assets/preload-helper-BjS5LQBu.js.br +0 -0
- package/ai.plurnk/dist/assets/preload-helper-BjS5LQBu.js.gz +0 -0
- package/ai.plurnk/dist/assets/session-BcdGbgAa.js +4 -0
- package/ai.plurnk/dist/assets/session-BcdGbgAa.js.br +0 -0
- package/ai.plurnk/dist/assets/session-BcdGbgAa.js.gz +0 -0
- package/ai.plurnk/dist/assets/session-agent-DwY8S586.js +111 -0
- package/ai.plurnk/dist/assets/session-agent-DwY8S586.js.br +0 -0
- package/ai.plurnk/dist/assets/session-agent-DwY8S586.js.gz +0 -0
- package/ai.plurnk/dist/assets/src-Du01yeMu.js +1 -0
- package/ai.plurnk/dist/assets/src-Du01yeMu.js.br +0 -0
- package/ai.plurnk/dist/assets/src-J2qT9xpv.js +6 -0
- package/ai.plurnk/dist/assets/src-J2qT9xpv.js.br +0 -0
- package/ai.plurnk/dist/assets/src-J2qT9xpv.js.gz +0 -0
- package/ai.plurnk/dist/icons/APACHE-2.0.txt +202 -0
- package/ai.plurnk/dist/icons/APACHE-2.0.txt.br +0 -0
- package/ai.plurnk/dist/icons/APACHE-2.0.txt.gz +0 -0
- package/ai.plurnk/dist/icons/NOTO-NOTICE.txt +10 -0
- package/ai.plurnk/dist/icons/NOTO-NOTICE.txt.br +0 -0
- package/ai.plurnk/dist/icons/NOTO-NOTICE.txt.gz +0 -0
- package/ai.plurnk/dist/icons/plurnk-maskable.svg +64 -0
- package/ai.plurnk/dist/icons/plurnk-maskable.svg.br +0 -0
- package/ai.plurnk/dist/icons/plurnk-maskable.svg.gz +0 -0
- package/ai.plurnk/dist/icons/plurnk.svg +63 -0
- package/ai.plurnk/dist/icons/plurnk.svg.br +0 -0
- package/ai.plurnk/dist/icons/plurnk.svg.gz +0 -0
- package/ai.plurnk/dist/index.html +421 -0
- package/ai.plurnk/dist/manifest.webmanifest +27 -0
- package/ai.plurnk/dist/manifest.webmanifest.br +0 -0
- package/ai.plurnk/dist/manifest.webmanifest.gz +0 -0
- package/ai.plurnk/dist/vendor/CFWORKER-JSON-SCHEMA-LICENSE.txt +25 -0
- package/ai.plurnk/dist/vendor/CFWORKER-JSON-SCHEMA-LICENSE.txt.br +0 -0
- package/ai.plurnk/dist/vendor/CFWORKER-JSON-SCHEMA-LICENSE.txt.gz +0 -0
- package/ai.plurnk/dist/vendor/DEPENDENCIES-LICENSE.txt +571 -0
- package/ai.plurnk/dist/vendor/DEPENDENCIES-LICENSE.txt.br +0 -0
- package/ai.plurnk/dist/vendor/DEPENDENCIES-LICENSE.txt.gz +0 -0
- package/ai.plurnk/dist/vendor/MARKED-FOOTNOTE-LICENSE.txt +25 -0
- package/ai.plurnk/dist/vendor/MARKED-FOOTNOTE-LICENSE.txt.br +0 -0
- package/ai.plurnk/dist/vendor/MARKED-FOOTNOTE-LICENSE.txt.gz +0 -0
- package/ai.plurnk/dist/vendor/OTM-LICENSE.txt +21 -0
- package/ai.plurnk/dist/vendor/OTM-LICENSE.txt.br +0 -0
- package/ai.plurnk/dist/vendor/OTM-LICENSE.txt.gz +0 -0
- package/ai.plurnk/dist/vendor/PICO-LICENSE.txt +21 -0
- package/ai.plurnk/dist/vendor/PICO-LICENSE.txt.br +0 -0
- package/ai.plurnk/dist/vendor/PICO-LICENSE.txt.gz +0 -0
- package/ai.plurnk/documents.js +483 -0
- package/ai.plurnk/http.js +69 -0
- package/ai.plurnk/index-document.js +5 -0
- package/ai.plurnk/module.js +326 -0
- package/ai.plurnk/session.js +319 -0
- package/ai.plurnk/static-assets.js +40 -0
- package/package.json +92 -5
- package/plugin.json +13 -0
package/SPEC.md
ADDED
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
# Web module
|
|
2
|
+
|
|
3
|
+
## §web-host Hosting
|
|
4
|
+
|
|
5
|
+
An Agent Plugin declares one native Plurnk module. It uses the public
|
|
6
|
+
module lifecycle and HTTP registration seam, never a separate listener or process.
|
|
7
|
+
The daemon owns routing precedence, startup readiness and shutdown.
|
|
8
|
+
|
|
9
|
+
| Configuration | Behavior |
|
|
10
|
+
|---|---|
|
|
11
|
+
| PLURNK_WEB_ENABLED=1 | Claim and register `/` on the daemon's existing listener. |
|
|
12
|
+
| PLURNK_WEB_ENABLED=0 | Inert module: no route claims or registration. |
|
|
13
|
+
| Invalid value | Existing configuration diagnostic; no web module activation. |
|
|
14
|
+
| Package absent | No web routes. |
|
|
15
|
+
|
|
16
|
+
The package's .env.defaults owns the default. More-specific mounts retain their ownership.
|
|
17
|
+
|
|
18
|
+
## §web-route-settlement Shutdown admission and settlement
|
|
19
|
+
|
|
20
|
+
| Boundary | Contract |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Stop | Refuse every new request to the module's mount with a private 503 `service-stopping` Problem, before accessing session or host resources. HEAD omits the body. Already admitted requests remain owned until their handlers settle, even if the browser disconnects. |
|
|
23
|
+
| Close | Stop admission, cancel diagram compilation, join all admitted handlers, then release session state and its credential observer. Close also works without a preceding stop or completed startup. |
|
|
24
|
+
| Failure | Unexpected handler errors still reject to the HTTP host; joining them does not turn them into success. A cleanup failure does not skip request settlement or subsequent resource cleanup. |
|
|
25
|
+
| Host | The daemon owns transport teardown and the overall shutdown deadline. Web adds neither a request-body deadline nor a separate shutdown timeout. No web request may reach host resources after close returns. |
|
|
26
|
+
|
|
27
|
+
## §web-configuration Configuration boundary
|
|
28
|
+
|
|
29
|
+
| Boundary | Contract |
|
|
30
|
+
|---|---|
|
|
31
|
+
| Module choices | `ai.plurnk/.env.defaults` declares enablement, browser approval mode, reasoning-preview retention, the sign-in time limit, workspace creation limits and diagram compilation limits. The daemon assembles that floor beneath operator sources; the shared `Knob` reader validates the effective values without reader-local defaults. |
|
|
32
|
+
| Invalid input | An invalid `PLURNK_WEB_ENABLED` refuses the module. Other invalid web settings are reported in `module.contained` and withhold only the part each configures (503 when requested), leaving the rest of the module operational. Missing floor values remain an internal deployment error; neither condition silently selects a replacement value. Disabled web does not inspect unused web settings. |
|
|
33
|
+
| Other owners | Consume daemon authentication and credential policy through their existing configuration and public seams, never web-owned substitutes. |
|
|
34
|
+
| Browser | Receives only explicit public document projections, not the environment, credentials or a second set of deployment defaults. Vite compiles assets independently of operator configuration. |
|
|
35
|
+
|
|
36
|
+
## §web-assets Static resources
|
|
37
|
+
|
|
38
|
+
| Request | Response |
|
|
39
|
+
|---|---|
|
|
40
|
+
| GET `/` | Home document rendered from the packaged HTML; authenticated header controls follow {§web-page-shell}. |
|
|
41
|
+
| GET `/index.html` | Same root document. |
|
|
42
|
+
| GET `/assets/<build-generated-name>` | Compiled browser JavaScript and CSS. |
|
|
43
|
+
| GET `/manifest.webmanifest` | Web app manifest (`application/manifest+json`). |
|
|
44
|
+
| GET `/icons/plurnk.svg` | Scalable app icon. |
|
|
45
|
+
| GET `/icons/plurnk-maskable.svg` | Maskable app icon. |
|
|
46
|
+
| GET `/icons/NOTO-NOTICE.txt`, `/icons/APACHE-2.0.txt` | Artwork attribution and license. |
|
|
47
|
+
| GET `/vendor/OTM-LICENSE.txt`, `/vendor/PICO-LICENSE.txt`, `/vendor/DEPENDENCIES-LICENSE.txt` | Packaged substrate and bundled JavaScript dependency licenses. |
|
|
48
|
+
| GET `/vendor/<PACKAGE>-LICENSE.txt` | The license text of a bundled package whose npm artifact omits it, retained from the package's published git revision and named for the package (`@scope/name` as `SCOPE-NAME`, uppercase); its `DEPENDENCIES-LICENSE.txt` entry is a heading only. |
|
|
49
|
+
| HEAD on any served path | Same status and headers as GET, without a body. |
|
|
50
|
+
| Other asset path | 404 error document, never a home-page fallback or filesystem listing. |
|
|
51
|
+
| Asset method other than GET/HEAD | 405 with `Allow: GET, HEAD`. |
|
|
52
|
+
|
|
53
|
+
Query strings do not select assets. Paths select the build-generated asset inventory, never
|
|
54
|
+
filesystem paths derived from the request. Only packaged public assets are served;
|
|
55
|
+
configuration, source, credentials and workspace files are not public resources.
|
|
56
|
+
Assets are loaded during module startup: missing package bytes fail startup rather
|
|
57
|
+
than leaving a partially functioning page. Responses declare their media type,
|
|
58
|
+
byte length and `X-Content-Type-Options: nosniff`.
|
|
59
|
+
|
|
60
|
+
## §web-asset-encoding Static content encoding
|
|
61
|
+
|
|
62
|
+
| Boundary | Contract |
|
|
63
|
+
|---|---|
|
|
64
|
+
| Build | Retain each original asset. For non-HTML assets, package Brotli and gzip representations only when smaller than the original. The inventory names those representations; compression runs at build time, not startup or per request. |
|
|
65
|
+
| Address | All representations share the original URL and media type. Encoded files are inventory implementation details, not additional public URLs. Every declared representation must load before route registration. |
|
|
66
|
+
| Negotiation | Use standard `Accept-Encoding` negotiation, honoring quality weights, explicit exclusions, wildcards and identity. Among equally acceptable representations prefer the smallest. An absent or empty header selects identity; no acceptable representation yields 406. |
|
|
67
|
+
| Headers | Add `Accept-Encoding` to `Vary`, preserving existing fields; `Content-Encoding` only for encoded representations; `Content-Length` is the selected representation's byte length. HEAD selects the same representation and headers as GET but sends no body. No new cache lifetime is introduced. |
|
|
68
|
+
| Scope | Dynamic/private documents, authentication responses and other daemon mounts, including ACP streams, are not compressed by this handler. Their existing security, caching and streaming contracts remain unchanged. |
|
|
69
|
+
|
|
70
|
+
Reference: [HTTP content negotiation](https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.3).
|
|
71
|
+
|
|
72
|
+
## §web-index-documents Directory documents
|
|
73
|
+
|
|
74
|
+
| Request | Resolution |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `/folder/` | The admitted `/folder/index.html` resource. |
|
|
77
|
+
| `/folder/index.html` | That same resource, with the same relative-link base. |
|
|
78
|
+
| `/` and `/index.html` | The root index document. |
|
|
79
|
+
| `/folder` | No implicit directory alias; links to directories include the final slash. |
|
|
80
|
+
| Missing index or child asset | 404, not an ancestor's index or the root document. |
|
|
81
|
+
|
|
82
|
+
Resolution uses the exact resource inventory, not a filesystem search or path
|
|
83
|
+
normalization. Query strings do not change the selected bytes. Production GET and
|
|
84
|
+
HEAD have the same status and headers; HEAD has no body. No redirect or history rewrite is
|
|
85
|
+
needed for the two index spellings. Native links, relative children, refresh and
|
|
86
|
+
back/forward work with JavaScript enabled or disabled. OTM does not intercept
|
|
87
|
+
document navigation; the server remains responsible for the document.
|
|
88
|
+
|
|
89
|
+
Production, Vite development and compiled preview share the same index-path
|
|
90
|
+
projection, applied before resource serving. Vite still owns HTML transformation,
|
|
91
|
+
compilation and HMR; no browser router is registered. Its development-only modules and
|
|
92
|
+
source serving are not production resources. Currently only the root document
|
|
93
|
+
is shipped by Vite. The daemon additionally renders the protected directories
|
|
94
|
+
below; unlisted assets remain unserved.
|
|
95
|
+
|
|
96
|
+
## §web-directory-addresses Named directory addresses
|
|
97
|
+
|
|
98
|
+
The directory codec maps names to origin-relative paths, not database IDs or
|
|
99
|
+
filesystem locations. It does not serve documents or authorize access.
|
|
100
|
+
|
|
101
|
+
| Directory | Generated path | Explicit document |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| Current unnamed daemon | `/~/` | `/~/index.html` |
|
|
104
|
+
| Workspace `~/ptl/project` | `/~/~/ptl/project/` | `/~/~/ptl/project/index.html` |
|
|
105
|
+
| Its session `reviewer` | `/~/~/ptl/project@reviewer/` | `/~/~/ptl/project@reviewer/index.html` |
|
|
106
|
+
| Workspace `a@b`, session `user` | `/~/a%40b@user/` | `/~/a%40b@user/index.html` |
|
|
107
|
+
| Workspace `index.html` | `/~/index.html/` | `/~/index.html/index.html` |
|
|
108
|
+
|
|
109
|
+
Workspace names are opaque, case-sensitive text. Encode each slash-separated
|
|
110
|
+
component with `encodeURIComponent`, preserving every slash, including leading,
|
|
111
|
+
repeated and trailing slashes. Encode the session as one component. Raw `@` is the
|
|
112
|
+
session delimiter: split it and the document suffix before decoding each name
|
|
113
|
+
exactly once. Literal `%2F` therefore becomes `%252F`, not a slash. Do not trim,
|
|
114
|
+
normalize Unicode, resolve names as paths, or interpret `~` as a home directory.
|
|
115
|
+
|
|
116
|
+
The parser accepts the folder and explicit-index spellings, percent-encoding
|
|
117
|
+
case variants and encoded equivalents of name characters (including `/`). `%7E`
|
|
118
|
+
is equivalent to the fixed `~` namespace marker. Query and fragment components
|
|
119
|
+
do not select a directory. Slashless directory aliases, malformed UTF-8/percent
|
|
120
|
+
escapes, empty names, multiple raw session delimiters, session child resources and
|
|
121
|
+
named-daemon prefixes are not directory matches. Parsing performs no lookups.
|
|
122
|
+
|
|
123
|
+
The daemon's [workspace name contract](https://github.com/plurnk/plurnk-service/blob/main/plurnk-core/SPEC.md)
|
|
124
|
+
admits nonempty well-formed Unicode without literal slash-delimited `.` or `..`
|
|
125
|
+
segments and reserves ASCII digit-only names for IDs. Admission does not rename existing
|
|
126
|
+
records: representable legacy names, including numeric names, retain their URLs.
|
|
127
|
+
The codec returns no path for an unrepresentable legacy name rather
|
|
128
|
+
than repairing it or substituting an ID; decoding an address to such a name is
|
|
129
|
+
also refused. Renaming changes its generated URL, not its identity or project
|
|
130
|
+
folder. No old-name redirect or filesystem exposure is implied.
|
|
131
|
+
|
|
132
|
+
This codec is verified using isolated native-document fixtures in production,
|
|
133
|
+
Vite development and preview. Live directory publication uses the daemon's
|
|
134
|
+
authentication contract.
|
|
135
|
+
|
|
136
|
+
## §web-session Browser sign-in
|
|
137
|
+
|
|
138
|
+
Web consumes the daemon's credential authority, never the operator's secret or
|
|
139
|
+
a second credential configuration. At setup it registers the confidential
|
|
140
|
+
client `plurnk-web` with callback path `/auth/callback` and scope `acp`. It asks
|
|
141
|
+
the published issuing face for the issuer of each request: the daemon's configured
|
|
142
|
+
public origin, else the address that request reached the daemon at. Its own calls to
|
|
143
|
+
the authority (discovery, code exchange, refresh) go to the face's answer for no
|
|
144
|
+
request, never to an address a browser named. No issuing face means the daemon has
|
|
145
|
+
explicitly disabled authentication; directory reads are then anonymous.
|
|
146
|
+
|
|
147
|
+
| Boundary | Contract |
|
|
148
|
+
|---|---|
|
|
149
|
+
| Authentication enabled, empty password | The authority shows configuration-required information and issues no browser sign-in, including on localhost. Web neither infers anonymous access nor adds a local bypass. |
|
|
150
|
+
| Authentication enabled, configured password | A browser signs in with `PLURNK_PASS` alone. No username field or web-owned identity configuration; a wrong password is refused by the authority at 401. |
|
|
151
|
+
| Authentication explicitly disabled | Anonymous directories; no Account or Sign Out controls. |
|
|
152
|
+
| GET `/auth/login?return=<page>` | Begin authorization code with PKCE S256 and a fresh state, carried sealed in that browser's own HttpOnly cookie. Return locations are local directory addresses, `/account/` or `/account/sessions/` (also their explicit indexes); never an external URL. |
|
|
153
|
+
| GET `/auth/callback` | Open this browser's transaction for the returned state and clear it whatever the outcome; validate and exchange the code with client authentication. A code the authority already redeemed answers 400. Issue a new authority `session` credential for the same subject, client, scopes and grant family, expiring with that grant's refresh credential. |
|
|
154
|
+
| Directory admission | Require a live authority-issued session for `plurnk-web` carrying `acp`; a bearer, refresh token, another client's cookie, or a revoked/expired session is not a web session. |
|
|
155
|
+
| POST `/auth/logout` | Require the issuer's exact Origin, revoke this browser's sign-in family and clear the web cookie. The authority sign-in and its module credentials end together; revisiting requires sign-in. Other browser sessions and independently authorized clients remain intact. |
|
|
156
|
+
| Concurrent callbacks | A new credential in the same sign-in family does not revoke that family. A genuinely different previous family is revoked. |
|
|
157
|
+
| Cookies | Host-only, HttpOnly, SameSite=Lax; Secure under HTTPS. Session path `/`, transaction path `/auth`. Under HTTPS the session cookie's name carries `__Host-` and a transaction's `__Secure-`, prefixes no sibling host can set. No browser storage or token in document content. |
|
|
158
|
+
| Request origin | A browser is answered at the address it reached the daemon by, and Origin checks use the daemon's issuer for that request. Sign-in alone starts at the issuer's address: a protected page sends a browser that is not signed in there, so its transaction cookie and callback share one host. No host is refused; forwarded headers are never read. |
|
|
159
|
+
| Failures | Invalid/replayed/mismatched callbacks fail without session issuance. Expiry/revocation requires sign-in again. Requests reveal no credential in diagnostics. |
|
|
160
|
+
|
|
161
|
+
A sign-in in progress lives only in its browser: the PKCE verifier, return page and
|
|
162
|
+
redirect address travel sealed (AES-256-GCM under a key that lives and ends with the
|
|
163
|
+
module instance) in an HttpOnly `/auth` cookie named for the transaction's state, for
|
|
164
|
+
`PLURNK_WEB_LOGIN_TTL_SEC`. The module keeps no state that an unauthenticated request can
|
|
165
|
+
fill, and a daemon restart ends the sign-ins in progress. Cookie and grant lifetimes come from the authority, not a
|
|
166
|
+
second lifetime setting. Sign-in uses the dependency-free MIT `oauth4webapi`
|
|
167
|
+
client and RFC 8414 discovery after listener admission; passwords are entered
|
|
168
|
+
only at the daemon's authorization endpoint.
|
|
169
|
+
Session observation uses {§web-session-observer}. Browser access credentials use
|
|
170
|
+
{§web-access-token}; they are never embedded in documents.
|
|
171
|
+
|
|
172
|
+
References: [OAuth browser applications](https://www.rfc-editor.org/rfc/rfc10017.html#section-6.2),
|
|
173
|
+
[OAuth security best practice](https://www.rfc-editor.org/rfc/rfc9700.html).
|
|
174
|
+
|
|
175
|
+
## §web-access-token Browser access credentials
|
|
176
|
+
|
|
177
|
+
The module is an OAuth token-mediating backend, not a second issuer. It retains
|
|
178
|
+
the authorization-code grant's access and refresh credentials in daemon memory,
|
|
179
|
+
keyed by the authority's sign-in family. The authority still owns admission,
|
|
180
|
+
identity, expiry, revocation and the account inventory.
|
|
181
|
+
|
|
182
|
+
| Boundary | Contract |
|
|
183
|
+
|---|---|
|
|
184
|
+
| POST `/auth/token` | Require a live web session and the issuer's exact Origin. Return only `access_token`, `token_type: Bearer`, and remaining `expires_in` seconds as no-store JSON. Other methods are 405; disabled authentication leaves the endpoint absent (404). No cross-origin access or credential-bearing redirect. |
|
|
185
|
+
| Access expiry | Reuse a still-valid access credential; otherwise perform the standard OAuth refresh grant with confidential-client authentication. Concurrent requests in the same family share one refresh, so rotation does not race across tabs. |
|
|
186
|
+
| Grant integrity | Access and refresh credentials must belong to the registered client, subject, scope and family. A refresh cannot change the family or resurrect a revoked sign-in. Failed refreshes are surfaced; no operation is replayed or authority substituted. |
|
|
187
|
+
| Revocation and cleanup | Authority revocation releases the retained grant. Expired grants are pruned on subsequent credential activity; module close releases them and its revocation subscription. Session expiry remains fixed by sign-in, not extended by refresh. |
|
|
188
|
+
| Browser custody | Access credentials are for the same-origin `/acp` endpoints and live only in JavaScript memory. Refresh credentials and client authentication never leave the server; no local/session storage, URL, document or diagnostic contains them. |
|
|
189
|
+
| Refusals | Missing/expired/revoked sign-in is 401; wrong/missing Origin is 403. Expected endpoint failures use RFC 9457 JSON, not an HTML sign-in redirect. |
|
|
190
|
+
|
|
191
|
+
Session observation consumes this primitive under {§web-session-observer}; it adds no
|
|
192
|
+
persistent browser state or additional session registry.
|
|
193
|
+
|
|
194
|
+
## §web-authorization-style Authorization document presentation
|
|
195
|
+
|
|
196
|
+
| Boundary | Contract |
|
|
197
|
+
|---|---|
|
|
198
|
+
| Registration | At setup the module registers the single stylesheet declared in the compiled document's head as its client's `stylesheetPath`, verifying its presence in the asset inventory. Missing or multiple document stylesheets are build-contract faults. Lazy content-renderer styles are independent assets, never authorization-page dependencies; no alias route or theme setting is added. |
|
|
199
|
+
| Document ownership | The authority owns sign-in, setup/error, consent and session documents, credential validation and security policy. Web supplies neither a form nor an HTML rewrite. The authority selects the stylesheet from the trusted client registration, never arbitrary request input. |
|
|
200
|
+
| Presentation | The same Pico Classless Green, monospace, 65ch measure, light/dark colors, native controls and article cards apply to authorization and web documents. Ordinary authorization pages work without JavaScript. |
|
|
201
|
+
| Production pruning | The public `authorizationDocumentSpecimens()` export from `@plurnk/plurnk-auth` supplies the authority's markup to PurgeCSS. No copied templates or private daemon-source scan. This is a build-time import, not a browser/runtime import. |
|
|
202
|
+
|
|
203
|
+
The daemon's standalone documents remain its own responsibility when no registered
|
|
204
|
+
module stylesheet applies. Browser coverage exercises the installed module and
|
|
205
|
+
authority together, including empty/wrong passwords and signed-out re-entry.
|
|
206
|
+
|
|
207
|
+
## §web-account Account and sign-ins
|
|
208
|
+
|
|
209
|
+
| Surface | Contract |
|
|
210
|
+
|---|---|
|
|
211
|
+
| GET/HEAD `/account/` or `/account/index.html` | Protected Pico article containing native navigation to Sessions. No displayed internal subject, fictional profile or inline session inventory. |
|
|
212
|
+
| GET/HEAD `/account/sessions/` or `/account/sessions/index.html` | Protected Pico article with Account breadcrumb and a native table of the authenticated subject's live sign-ins from `credentials.sessions(subject)`. No independent inventory. Table overflow stays inside its figure on narrow screens. |
|
|
213
|
+
| Each sign-in | Authority-issued creation/expiry times and client names; mark the family of the presented web session as “This session”. Do not invent device identity or last activity. Family handles are not credentials. |
|
|
214
|
+
| POST `/account/sessions/<family>/revoke` | Require a live web session and the issuer's exact Origin; revoke only a family in that subject's live inventory. A missing or foreign handle is 404. Revoking this session also clears the web cookie and returns home; otherwise redirect to Sessions. |
|
|
215
|
+
| POST `/account/sessions/revoke-others` | Same admission and Origin check; revoke all listed families except this session, then redirect to Sessions. |
|
|
216
|
+
| Expired/revoked credentials | Protected GET requires sign-in again; a submitted action fails at 401 rather than silently authenticating and replaying it. |
|
|
217
|
+
| Explicit anonymous posture | Account documents and actions are absent (404); no fictional account. |
|
|
218
|
+
|
|
219
|
+
Forms work with JavaScript enabled or disabled. Read and mutation responses use
|
|
220
|
+
the private headers in {§web-directory-documents}; GET never revokes anything.
|
|
221
|
+
Revocation does not mutate sessions, workspace ownership or independently authorized
|
|
222
|
+
client credentials. The daemon owns credential lifetime, grouping and revocation.
|
|
223
|
+
|
|
224
|
+
## §web-directory-documents Read-only directory documents
|
|
225
|
+
|
|
226
|
+
| Document | Content / lookup |
|
|
227
|
+
|---|---|
|
|
228
|
+
| `/` | Public foundation document and a real link to `/~/` when unauthenticated; live daemon-wide session overview from `readSessionOverview()` under {§web-session-directory} when admitted or in explicit anonymous posture. |
|
|
229
|
+
| `/~/` | `listWorkspaces()`: native list articles for the shown workspaces with exact-name links under {§web-page-shell}, and the folderless creation form under {§web-workspace-creation}. CSS prefixes names with ☁️ when `project_root` is null, otherwise 🗂️; article names identify folderless/project workspaces independently of glyphs. Never infer folder presence from the name. |
|
|
230
|
+
| Workspace directory | Exact-name lookup in `listWorkspaces()`, then folder presentation under {§web-workspace-folder} and one `readSessionOverview(id)` census rendered under {§web-session-directory}. |
|
|
231
|
+
| Session directory | Exact-name model-session lookup; identity, breadcrumbs and actual parent under {§web-session-directory}, plus passive status under {§web-session-observer} and conversation under {§web-session-conversation}. |
|
|
232
|
+
| Archived workspace/session | The daemon's `archived_at` mark leaves a workspace out of `/~/` and a session, with the sessions beneath it, out of its workspace's tree; its own page still opens by its exact address. |
|
|
233
|
+
| Missing workspace/session | 404; never create, attach, claim ownership, start a loop or infer a project folder. |
|
|
234
|
+
| Unrepresentable legacy name | Escaped text without a link; no renamed display, ID substitution or broken generated URL. |
|
|
235
|
+
|
|
236
|
+
Every directory (including a missing one) is authenticated before data lookup.
|
|
237
|
+
GET and HEAD have matching status/headers; HEAD has no body. Documents and auth
|
|
238
|
+
responses are `Cache-Control: no-store`, suppress referrers, prevent framing and
|
|
239
|
+
use same-origin scripts, styles and forms. Names are escaped as text/attribute
|
|
240
|
+
values; never executable HTML or filesystem paths. Ordinary navigation and
|
|
241
|
+
logout use native links/forms with JavaScript enabled or disabled. Rendering
|
|
242
|
+
reuses the compiled root document's head, styles, icons and OTM boot; no browser
|
|
243
|
+
router, client-side list fetch, duplicate English dictionary or extra UI library.
|
|
244
|
+
|
|
245
|
+
Vite development/preview continue to serve frontend documents only: dynamic
|
|
246
|
+
directory/auth routes require a daemon. The installation gate accepts explicitly
|
|
247
|
+
supplied published or candidate platform archives to verify the public contract.
|
|
248
|
+
|
|
249
|
+
## §web-workspace-folder Workspace and folder labels
|
|
250
|
+
|
|
251
|
+
Compare the workspace name with the daemon's `project_label` by exact equality.
|
|
252
|
+
Render that label as escaped text, never a filesystem link. Do not derive a label
|
|
253
|
+
from `project_root`, the browser's location, the client's home or the workspace name.
|
|
254
|
+
The daemon owns home abbreviation; names remain opaque and unchanged.
|
|
255
|
+
|
|
256
|
+
| Workspace page | Breadcrumb | Separate folder line before the session list |
|
|
257
|
+
|---|---|---|
|
|
258
|
+
| Folderless (`project_label: null`) | ☁️ name | None. |
|
|
259
|
+
| Name equals folder label | 🗂️ name | None; the breadcrumb already identifies the folder. |
|
|
260
|
+
| Name differs from folder label | ☁️ name | 🗂️ followed by the supplied label. |
|
|
261
|
+
|
|
262
|
+
The workspace index continues to mark folder presence under
|
|
263
|
+
{§web-directory-documents}, irrespective of name equality. Accessible icon labels
|
|
264
|
+
distinguish the workspace identity from its project folder; a cloud on a named
|
|
265
|
+
workspace with a separate folder must not claim it is folderless.
|
|
266
|
+
## §web-workspace-creation Folderless workspace creation
|
|
267
|
+
|
|
268
|
+
The daemon workspaces directory (`/~/`) header carries a native creation trigger button (`➕`);
|
|
269
|
+
individual workspace, session, account and error documents never do. Activating the trigger
|
|
270
|
+
opens a native `<dialog>` modal (`#dialog-workspace-create`). When JavaScript is disabled, the form
|
|
271
|
+
renders in normal flow at the end of `<main>` and submits cleanly without client scripting.
|
|
272
|
+
|
|
273
|
+
| Boundary | Contract |
|
|
274
|
+
|---|---|
|
|
275
|
+
| Form | A native modal (`#dialog-workspace-create`) containing one `method="post" action="/workspaces/new"` form (`#workspace-create`) with Cancel and Confirm buttons, an empty, optional text input named `name` whose description says the daemon names an unnamed workspace, and exactly one hidden token field containing a fresh UUID. An empty name is omitted from `createWorkspace`, and the daemon names the workspace; any other value is requested verbatim, for the daemon's admission to accept or refuse. Cancel or Escape dismisses without mutating state. With scripting, a resting `<main>` on `/~/` shows only directory content. |
|
|
276
|
+
| Folder independence | Creation calls the public `createWorkspace` with `projectRoot: null`, never inheriting another workspace's folder, the daemon's working directory or an operator home. It starts no loop, session or inference, copies no history, creates no project directory, and leaves other workspaces untouched. |
|
|
277
|
+
| Admission and origin | With authentication enabled, an unadmitted POST answers 401 and a cross-origin form 403 against the daemon's issuer. `PLURNK_AUTH=0` is a first-class anonymous posture, not a read-only mandate: creation requires a matching same-origin `Origin` header naming the request's own host, and any `sec-fetch-site` other than same-origin is refused with 403. |
|
|
278
|
+
| Method and encoding | Only POST is allowed; GET and HEAD answer 405 with `allow: POST`. The body is UTF-8 `application/x-www-form-urlencoded` (415 otherwise), bounded by `PLURNK_WEB_FORM_BYTES` (413), with exactly one `token` field (400 otherwise). |
|
|
279
|
+
| Convergence | The hidden token is the form's idempotency key at the module's HTTP boundary, because the daemon's creation contract has none. Token tracking begins on `POST /workspaces/new`. The first POST creates one workspace and answers 303 to its canonical address through the shared directory codec. Racing or retried POSTs of the same valid token converge to that same workspace, never a second creation. A settled token stays addressable for the configured TTL, so a retry after a lost response converges. An invalid UUID format answers 400. |
|
|
280
|
+
| Failure | A daemon refusal answers with the daemon's own status, detail and recovery; a daemon that failed answers 502. Either response offers the form again holding the person's draft and a fresh token, so a corrected submission is a new request, while the refused token's settled failure is not re-executed within its TTL. An envelope that names a project folder for a folderless request, or a workspace name without a directory address, is a visible internal violation, never a silently adapted creation. |
|
|
281
|
+
| Bound | In-flight and settled tokens are pruned on expiry and bounded across actual POST submissions: exceeding `PLURNK_WEB_CREATE_TOKENS` evicts the oldest settled completed outcome. In-flight creations remain protected during convergence. |
|
|
282
|
+
|
|
283
|
+
## §web-workspace-rename Workspace rename
|
|
284
|
+
|
|
285
|
+
Workspace page (`/~/<workspace>/`) headers carry a native rename trigger button (`✏️`);
|
|
286
|
+
daemon, session, account and error documents never do. Activating the trigger opens a native
|
|
287
|
+
`<dialog>` modal (`#dialog-workspace-rename`). When JavaScript is disabled, the form renders in normal
|
|
288
|
+
flow at the end of `<main>` and submits cleanly.
|
|
289
|
+
|
|
290
|
+
| Boundary | Contract |
|
|
291
|
+
|---|---|
|
|
292
|
+
| Form | A native modal (`#dialog-workspace-rename`) containing one `method="post" action="/workspaces/rename"` form with the current workspace name in a hidden `workspace` field, a required text input named `name` pre-populated with the current name, and Cancel and Confirm buttons. Dismissing via Cancel or Escape performs no mutation. With scripting, a resting `<main>` shows only directory content. |
|
|
293
|
+
| Delivery | Uses the public `renameWorkspace(workspaceId, name)` contract on `ApplicationPort`. Missing workspace answers 404; empty name answers 400. A refusal or failure answers as {§web-workspace-creation}'s Failure row says, offering the form again with the draft. Success 303-redirects to the canonical new workspace directory address through the shared directory codec. |
|
|
294
|
+
| Origin and method | Only POST is permitted; GET and HEAD answer 405 with `Allow: POST`. Same-origin enforcement requires matching `Origin` and same-origin `sec-fetch-site` in both auth and anonymous postures. |
|
|
295
|
+
|
|
296
|
+
## §web-session-creation Session creation
|
|
297
|
+
|
|
298
|
+
Workspace page (`/~/<workspace>/`) headers carry a native session creation trigger button (`➕`);
|
|
299
|
+
daemon, session, account and error documents never do. Activating the trigger opens a native
|
|
300
|
+
`<dialog>` modal (`#dialog-session-create`). When JavaScript is disabled, the form renders in normal
|
|
301
|
+
flow at the end of `<main>` and submits cleanly.
|
|
302
|
+
|
|
303
|
+
| Boundary | Contract |
|
|
304
|
+
|---|---|
|
|
305
|
+
| Form | A native modal (`#dialog-session-create`) containing one `method="post" action="/sessions/new"` form with the current workspace name in a hidden `workspace` field, a hidden UUID `token` field, an empty, optional text input named `name` whose description says the daemon names an unnamed session, and Cancel and Confirm buttons. Dismissing via Cancel or Escape performs no mutation. With scripting, a resting `<main>` shows only directory content. |
|
|
306
|
+
| Delivery | Uses the public `createSession({ workspaceId, name? })` contract on `ApplicationPort`, creating an independent model session without inheriting parentage, loops, transcripts, or forking active state. An empty name is omitted and the daemon names the session; any other value is requested verbatim. |
|
|
307
|
+
| Convergence | The form token acts as idempotency key: racing double-clicks and retries converge to the single created session. Expiry and bounds share the creation limits. |
|
|
308
|
+
| Success and failure | Missing workspace answers 404. Success 303-redirects to the new session's canonical directory address (`directoryPath({ kind: "session", workspace, session })`). A refusal or failure answers as {§web-workspace-creation}'s Failure row says; a session name without a directory address is an internal violation, not a refusal. |
|
|
309
|
+
| Origin and method | Only POST is permitted; GET and HEAD answer 405 with `Allow: POST`. Strict same-origin enforcement applies in both auth and anonymous postures. |
|
|
310
|
+
## §web-workspace-archive Workspace archive
|
|
311
|
+
|
|
312
|
+
Workspace page (`/~/<workspace>/`) headers carry a native archive trigger button (`🗑️`);
|
|
313
|
+
daemon, session, account and error documents never do. Activating the trigger opens a native
|
|
314
|
+
`<dialog>` modal (`#dialog-workspace-archive`). When JavaScript is disabled, the form renders in normal
|
|
315
|
+
flow at the end of `<main>` and submits cleanly.
|
|
316
|
+
|
|
317
|
+
| Boundary | Contract |
|
|
318
|
+
|---|---|
|
|
319
|
+
| Form | A native modal (`#dialog-workspace-archive`) containing one `method="post" action="/workspaces/archive"` form with the current workspace name in a hidden `workspace` field, a message explaining that archiving leaves files and history intact, and Cancel and Confirm buttons. Dismissing via Cancel or Escape performs no mutation. |
|
|
320
|
+
| Delivery | Uses the public `setWorkspaceArchived({ workspaceId, archived: true })` contract on `ApplicationPort`. Missing workspace answers 404. A refusal (such as live work) answers with the daemon's status and detail. Success 303-redirects to `/~/`. |
|
|
321
|
+
| Origin and method | Only POST is permitted; GET and HEAD answer 405 with `Allow: POST`. Strict same-origin enforcement applies in both auth and anonymous postures. |
|
|
322
|
+
|
|
323
|
+
## §web-session-archive Session archive
|
|
324
|
+
|
|
325
|
+
Session page (`/~/<workspace>@<session>/`) headers carry a native archive trigger button (`🗑️`);
|
|
326
|
+
daemon, workspace, account and error documents never do. Activating the trigger opens a native
|
|
327
|
+
`<dialog>` modal (`#dialog-session-archive`). When JavaScript is disabled, the form renders in normal
|
|
328
|
+
flow at the end of `<main>` and submits cleanly.
|
|
329
|
+
|
|
330
|
+
| Boundary | Contract |
|
|
331
|
+
|---|---|
|
|
332
|
+
| Form | A native modal (`#dialog-session-archive`) containing one `method="post" action="/sessions/archive"` form with the current workspace name in a hidden `workspace` field, the session name in a hidden `session` field, an explanation that archiving removes the session and its descendants without destroying history, and Cancel and Confirm buttons. Dismissing via Cancel or Escape performs no mutation. |
|
|
333
|
+
| Delivery | Uses the public `setSessionArchived({ workspaceId, sessionId, archived: true })` contract on `ApplicationPort`. Missing workspace or session answers 404. A refusal (such as live work covered by the subtree) answers with the daemon's status and detail. Success 303-redirects to the workspace address (`directoryPath({ kind: "workspace", workspace })`). |
|
|
334
|
+
| Origin and method | Only POST is permitted; GET and HEAD answer 405 with `Allow: POST`. Strict same-origin enforcement applies in both auth and anonymous postures. |
|
|
335
|
+
|
|
336
|
+
## §web-session-directory Model-session navigation
|
|
337
|
+
| Boundary | Representation |
|
|
338
|
+
|---|---|
|
|
339
|
+
| Membership | Every shown `origin: model` session (`archived_at: null`) appears once, regardless of owner, lifecycle or name spelling. Non-model bookkeeping sessions have no directory row or session page; their direct page URL returns 404. This is presentation, not an access policy for daemon APIs. |
|
|
340
|
+
| Roots | Shown model sessions with `parentSessionId: null` form the primary list. Sessions with a delivered reply (`done` or `fail`) order first by most recent delivered reply (`reply.id` descending); sessions with no delivered reply follow in daemon order. On `/`, roots span all non-archived workspaces. |
|
|
341
|
+
| Children | Immediate model children nest beneath their actual model parent, ordered by most recent delivered reply (`reply.id` descending) with no-reply siblings following in daemon order. |
|
|
342
|
+
| Card identity | Two lines: a named ant link to the session, its lifecycle glyph, optional actual parent followed by `>`, and the session name; then a folder/cloud marker and a real link to its workspace. Model parents are links; bookkeeping parents are escaped text, never fabricated session destinations. Folder presence comes from `project_root`, not the workspace name. Kind and permission owner do not become visible metadata rows. |
|
|
343
|
+
| Lifecycle | Project the supplied lifecycle with the same glyph vocabulary as the session statusline; the icon has a literal accessible name/title. With JavaScript, the ACP `session/list` overview subscription keeps lifecycle, card order, membership and workspace labels live in place. |
|
|
344
|
+
| Background | Model sessions with an immediate non-model parent begin branches in one collapsed “Background work” section, ordered by most recent delivered reply. Omit it when empty. No A2A-specific classification or reparenting. |
|
|
345
|
+
| Disclosure | Each card is `li > article`. When it has children or a delivered reply, one initially closed `details` contains its two-line identity as a phrasing-only `summary`, followed by any nested `ul/li` child cards and a `[data-session-reply]` outlet carrying the reply's `/acp/<workspace ID>/<session ID>/entry` address. Leaves without a delivered reply use a `header` with the same identity and no empty disclosure. Activating a summary opens the card; when opened, JavaScript fetches the reply's entry body once on demand and renders it as sanitized Markdown, offering a native Retry form on failure and ignoring stale reads if a newer reply arrives. Activating its ant, parent or workspace link navigates without toggling it. Native keyboard and pointer behavior work without JavaScript; no separate child-count teaser. |
|
|
346
|
+
| Parent on session page | Show the actual parent's name: a link for a model parent, escaped text for a bookkeeping parent. Roots omit the parent line. URL breadcrumbs remain workspace/session navigation, not invented lineage paths. |
|
|
347
|
+
| Placement | Complete session trees belong to the home overview and workspace pages. Session pages retain breadcrumb and parent navigation, and use {§web-session-descendants} for live descendant navigation; no duplicated page-load tree. |
|
|
348
|
+
| Integrity | Duplicate identities, absent referenced parents and cyclic model parentage are internal contract failures; never silently omit or invent sessions. |
|
|
349
|
+
|
|
350
|
+
Rendering and expansion perform no attachment, inference, ownership change or
|
|
351
|
+
daemon mutation. Opening a card with a delivered reply fetches only that reply's
|
|
352
|
+
read-only entry body on demand; live `_plurnk/overview` updates reconcile cards
|
|
353
|
+
in place while preserving open disclosures, focus and already-rendered reply
|
|
354
|
+
bodies. Opening a session page may observe it under {§web-session-observer},
|
|
355
|
+
never control it.
|
|
356
|
+
|
|
357
|
+
## §web-session-observer Read-only session status
|
|
358
|
+
|
|
359
|
+
The server renders a semantic status footer only for an existing model session,
|
|
360
|
+
with the daemon's qualified session identity (`<workspace ID>#<session name>`) and
|
|
361
|
+
whether access requires a browser session. URLs and breadcrumbs retain readable names;
|
|
362
|
+
renaming a workspace does not redirect an already-open observation to another session.
|
|
363
|
+
No credential appears in the document. JavaScript enhances that footer on session
|
|
364
|
+
pages and subscribes to read-only `session/list` overview updates on home and
|
|
365
|
+
workspace pages; other pages neither load the ACP client nor request access
|
|
366
|
+
credentials or ACP streams.
|
|
367
|
+
Without JavaScript the identity and navigation remain usable, with an explicit
|
|
368
|
+
notice that live status requires JavaScript.
|
|
369
|
+
|
|
370
|
+
```mermaid
|
|
371
|
+
stateDiagram-v2
|
|
372
|
+
[*] --> Connecting
|
|
373
|
+
Connecting --> Connected: authoritative status
|
|
374
|
+
Connected --> Connecting: session terminal
|
|
375
|
+
Connected --> Connecting: configured automatic approval
|
|
376
|
+
Connecting --> Disconnected: refusal / transport or intake failure
|
|
377
|
+
Connected --> Disconnected: shutdown / transport or intake failure
|
|
378
|
+
Connected --> Interrupted: input required, observation stays live
|
|
379
|
+
Interrupted --> Connecting: another client settles input
|
|
380
|
+
Interrupted --> Connecting: explicit review / reconnect / Stop
|
|
381
|
+
Disconnected --> Connecting: explicit reconnect
|
|
382
|
+
Connecting --> SignedOut: sign-in refused
|
|
383
|
+
Connected --> SignedOut: credential revoked
|
|
384
|
+
Connecting --> Closed: page departure
|
|
385
|
+
Connected --> Closed: page departure
|
|
386
|
+
Closed --> Connecting: browser restores page
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
| Boundary | Contract |
|
|
390
|
+
|---|---|
|
|
391
|
+
| Transport | A fresh official ACP client using `/acp` (`session/resume` with `replayFrom: { type: "start" }`) and the qualified `sessionId` supplied by the document. No forwarded workspace, action, control claim or capability replacement. A pending review keeps its current client instance and open ACP connection until its held `session/request_permission` or `elicitation/create` requests settle. The SDK and `SessionTranslator` own notification parsing, replay and state updates; a fresh attachment rediscovers current conversation and interrupt continuations. |
|
|
392
|
+
| Credentials | Before each connection, use {§web-access-token}; explicitly anonymous documents send no Authorization header and never call `/auth/token`. An admitted stream outlives access expiry. Credentials live only in the active request/client instance and are released on departure. |
|
|
393
|
+
| Continuity | Standard successful/cancelled turn responses (`stopReason: end_turn | cancelled`), or a `state_update` / JSON-RPC error carrying a session terminal in `data.plurnk.terminal`, ends the delivery; reconnect passively to watch subsequent work. A held observation may finish when its gate settles, before the session concludes: only daemon status/terminal evidence establishes the session outcome. A protocol/credential failure stops observation. An interrupt applies {§web-session-input}: configured approval or explicit input required. Never retry work or infer completion from transport closure. |
|
|
394
|
+
| Disconnection | Transport/protocol failure, admission refusal, or daemon shutdown stops reconnection. Show a Pico error card with the failure's diagnostic, retain clearly stale last-observed values, and offer a native Reconnect form. Credential-response failures describe the status or invalid format, never response bytes or headers. Its enhanced submission reattaches without navigation, preserving the draft; it never resubmits input. A revoked/expired browser session offers native sign-in instead. No background polling or retry/backoff system. |
|
|
395
|
+
| Intake | A notification the page cannot take in (an invalid Problem, notice, stream outcome, activity row or history page) ends the observation as a protocol failure, with that failure's diagnostic. |
|
|
396
|
+
| Page lifetime | `pagehide` closes the observation and stops local timers without cancelling the session. A restored page opens a fresh observation. Hidden tabs do not claim or cancel work. |
|
|
397
|
+
| Projection | Public state supplies identity under {§web-session-identity}, status under {§web-session-status}, and the complete descendant census under {§web-session-descendants}. The browser keeps no accounting ledger, inferred topology or transcript cache. |
|
|
398
|
+
| Elapsed | Measure the latest loop from daemon `claimedAt` to `terminatedAt`, or the current clock while connected. Update seconds locally; freeze on disconnect. No claim time means unknown. Local clock arithmetic never changes daemon lifecycle or accounting. |
|
|
399
|
+
| Presentation | The compact footer under {§web-session-status} uses native HTML and OTM text/state projection, ordinary light/dark Pico styling, no application classes or imperative styling. Connection changes and errors are announced separately; ticking gauges are not live announcements. A healthy connection needs no persistent “Connected” label. |
|
|
400
|
+
|
|
401
|
+
Opening the page submits no message and cancels no work. Existing owner proposals
|
|
402
|
+
may receive the configured response under {§web-session-approval}. Message
|
|
403
|
+
submission and explicit cancellation belong to {§web-session-input}.
|
|
404
|
+
|
|
405
|
+
## §web-session-identity Current names, fixed observation
|
|
406
|
+
|
|
407
|
+
| Boundary | Contract |
|
|
408
|
+
|---|---|
|
|
409
|
+
| Binding | The document's qualified session ID is immutable for its lifetime. Workspace state must identify that same workspace; a different ID is a contract failure, not permission to retarget observation. |
|
|
410
|
+
| Names and navigation | Each supplied workspace state refreshes the workspace breadcrumb, document title, project-folder label and workspace-relative navigation destinations using the shared directory codec. Session and parent names remain document facts. An idle connection does not discover a rename until the daemon supplies updated state or a new attachment snapshot. No polling or browser name registry. |
|
|
411
|
+
| Folder | Apply {§web-workspace-folder} on session pages too: compare the supplied name and folder label exactly, never abbreviate or resolve filesystem paths in the browser. |
|
|
412
|
+
| Location | Keep the current URL and history unchanged. Current breadcrumb and parent links use the latest supplied workspace name; sign-in and reconnect destinations follow it too. An old URL still returns 404 after a rename, rather than becoming an identity alias. |
|
|
413
|
+
|
|
414
|
+
## §web-session-status Live session and workspace facts
|
|
415
|
+
|
|
416
|
+
| Surface | Projection |
|
|
417
|
+
|---|---|
|
|
418
|
+
| Document | A body-level footer contains status above the composer. Center two wrapping groups: automatic approval/model/effort, then the remaining statistics with cost last. Each group fits the available width; a long model route can occupy its own row above statistics. No repeated workspace/session identity or metadata inventory. Each field has an accessible name independent of CSS-generated glyphs and separators. The SDK retains full state; the DOM is not a second dataset. |
|
|
419
|
+
| Visibility | Hide the status row without reserving space unless the session is queued, running or parked, focus is within the footer, or its model/effort selector is open. Keep it present through pointer activation of its controls. An unknown lifecycle still supplies no status row. Composer and connection feedback remain independent. Native focus/disclosure state and existing lifecycle attributes own visibility through CSS, without another JavaScript state or timer. |
|
|
420
|
+
| Session | `plurnk.status` supplies lifecycle and resolved model/effort. Show the alias, otherwise provider/model, with supplied effort and no decorative model glyph; retain the full route and effort source in its description. A null model says “No model”; absent effort uses a neutral placeholder so its selector remains reachable. Completed lifecycle has no visible indicator or reserved space; other supplied states retain theirs. Page-local automatic approval uses a native toggle button: pressed with 🔥 when enabled, unpressed with 🐹 for manual review. Its stable accessible name is “Automatic approval”; CSS supplies the glyph. It shares `/yolo` behavior under {§web-session-commands}, without submitting the draft or adding a command-history entry. Descendant counts and pending gates follow {§web-session-descendants}; neither changes the bound session's lifecycle. No provider fallback. |
|
|
421
|
+
| Clocks | Elapsed follows {§web-session-observer}, not the browser session, and is omitted without a claim timestamp. `wait` shows whole seconds remaining, rounded up and clamped to zero, with the supplied deadline on its native time element. A null deadline clears it. Expiry neither changes lifecycle nor claims completion or resumption. Both clocks repaint only while connected or awaiting input, and freeze when delivery is unavailable. |
|
|
422
|
+
| Context | Root `budget` supplies context occupancy/capacity. Show only percentage/capacity (for example, `11%/650k`) when both values are known and capacity is positive; descriptions retain exact supplied values. Curation stays in SDK state, without a document projection. Unknown input clears the field, never retains an older snapshot or substitutes accounting totals or model configuration. |
|
|
423
|
+
| Accounting | Show daemon cumulative session-and-descendant input/output tokens and USD cost, never a page-local ledger. Cost is the last list item in document order. Tokens use compact notation; cost uses four fractional digits, with a nonzero amount below display precision shown as less than that precision, never zero. Missing totals show `?`; a supplied known subtotal may precede `+ ?` but never becomes a total. Accessible descriptions retain exact reported values and distinguish partial from complete evidence. No summing, estimated prices or inferred token partitions; formatting never mutates SDK state. |
|
|
424
|
+
| Workspace activity | Show current preparation phase and family/alias, and indexing phase with the supplied percentage when present. Retain the indexing message as a description, without reconstructing progress or printing the full activity record. Empty preparation and null activity clear their fields. These are workspace facts, not session lifecycle or conversation messages; a session terminal alone does not clear them. |
|
|
425
|
+
| Inbox | Show a count only when accepted-unobserved messages exist. It describes observation, not execution or a promise that a terminated loop will consume them. Do not render message IDs, paths or receiving-loop records as a status inventory. |
|
|
426
|
+
| Continuity and admission | Project the SDK's complete state after snapshots and deltas. Native disclosures remain open across updates; names, messages and other supplied strings are literal text. Disconnected values remain visibly stale under {§web-session-observer}. Observation, timer ticks and disclosure do not submit actions or initiate extra network requests. |
|
|
427
|
+
|
|
428
|
+
## §web-session-descendants Live descendant navigation
|
|
429
|
+
|
|
430
|
+
| Boundary | Contract |
|
|
431
|
+
|---|---|
|
|
432
|
+
| Authority | `status.descendants` is the daemon's complete census beneath the bound session. Render every supplied session once, nested by `parentSessionId`, preserving sibling order; do not infer sessions or activity from transcripts, query the directory, or merge successive censuses. This live subtree is distinct from the workspace navigation snapshot under {§web-session-directory}. |
|
|
433
|
+
| Counts | Alive direct children are roots of that census whose lifecycle is queued, running or parked. “Descendants awaiting input” counts sessions at any depth with `gates > 0`, not the number of gates. Show each session's exact gate count beside its kind and lifecycle; a pending gate does not change its lifecycle locally. |
|
|
434
|
+
| Document | A native disclosure in the footer shows the descendant count, describing active direct children in its accessible name. It is initially closed and absent from presentation when the census is empty, with no placeholder. Opening it reveals nested `ul/li` lists, real named session links and separate child disclosures; leaves have no child disclosure. Names and facts are literal text. Destinations use the current supplied workspace name and shared directory codec. |
|
|
435
|
+
| Continuity | A replacement census updates membership, facts and counts. Every surviving session keeps its row's node, and with it focus and an open child disclosure, whether or not its facts changed; only a row out of place moves, and new branches start collapsed. Workspace renames update links without retargeting the bound session or changing browser history. Disconnection leaves visibly stale evidence, not an invented empty tree. |
|
|
436
|
+
| Integrity | Duplicate identities, cycles, or roots pointing to different absent parents are contract failures. A census may reference only its one excluded bound session as the roots' parent. No silent reparenting, dropped sessions, hierarchy inferred from names or client-owned lifecycle. |
|
|
437
|
+
|
|
438
|
+
## §web-session-diagnostics Observed notices and Problems
|
|
439
|
+
|
|
440
|
+
| Boundary | Contract |
|
|
441
|
+
|---|---|
|
|
442
|
+
| Intake | The current ACP client's `onSessionUpdate` receives `notice` updates carrying `_meta.plurnk.notice` or `_meta.plurnk.problem`. Validate each through its public contracts validator. Unknown sessionUpdate variants are not interpreted as diagnostics. These nonterminal updates never change lifecycle, end observation, resume an interrupt or enter the conversation. |
|
|
443
|
+
| Document | A separate native list uses {§web-card-disclosure}. Its header contains the literal Notice message (otherwise source/kind) or Problem title beside its severity glyph. Distinct Problem detail/recovery is an initially collapsed literal body, without duplicating the title. Accessible naming conveys severity, source/kind or title and supplied subagent attribution. Entry attributes carry relevant semantic facts; no glyph markup or full-payload disclosure. Hide the section when it has no entries. |
|
|
444
|
+
| Retention | The page retains distinct observed diagnostics across delivery changes and reconnects. An exact repeat of the update kind, payload serialization and subagent attribution reuses its existing entry. Deduplication evidence stays in JavaScript, not DOM storage. This is neither an occurrence count nor a durable diagnostic history: reload starts from what the daemon supplies again. No browser storage, transcript reconstruction or inferred error resolution. |
|
|
445
|
+
| Authority | Notice severity is producer-supplied; an error-level Notice or nonterminal Problem is not a failed session. Connection errors and operation results keep their existing owners. The diagnostic list does not claim to contain events from before attachment or from unobserved children. |
|
|
446
|
+
|
|
447
|
+
## §web-session-activity Observed non-conversation activity
|
|
448
|
+
|
|
449
|
+
| Boundary | Contract |
|
|
450
|
+
|---|---|
|
|
451
|
+
| Intake | The current ACP client supplies `tool_call_update` client-facing log rows and `agent_message_chunk` text emitted outside operations via `_meta.plurnk`. They remain separate from conversation messages, reasoning previews, diagnostics, approvals and lifecycle. Unknown sessionUpdate families are ignored. Invalid identity or displayed fields fail at intake, not as empty activity. |
|
|
452
|
+
| Document | A native “Observed activity” disclosure contains an unnumbered `ul` list of articles using {§web-operation-summary} and {§web-card-disclosure}, with the authored body as literal text and, when supplied, a collapsed reasoning disclosure. These observations start collapsed, including outside text; they are evidence, not delivered replies. Its accessible name identifies operation, session, coordinate and result state. Relevant identity, origin, coordinate, state, status and attribution live on the entry as scalar attributes. Outside text is literal content with accessible “Outside operations” attribution and coordinate, never an assistant answer or a copy in data attributes. No inferred names, session tree, heading parsing or executable markup. |
|
|
453
|
+
| Duplication | Ambient rows use their session and row IDs plus supplied subagent attribution; outside text uses its coordinate plus attribution. Repeated delivery updates that entry in first-observation order. Unchanged evidence preserves its nodes and inspected bodies. Deduplication and reference-render bookkeeping stay in JavaScript, not DOM storage. Changed references replace only their inspection outlets. The `_plurnk` `read` row with `attrs.kind=emission` is omitted because its announced operations already have their own conversation representation; rejected emissions and other ambient rows are not suppressed. |
|
|
454
|
+
| Content | Operation summaries do not fetch receipt bodies. Referenced reasoning uses {§web-result-bodies} and stays unread until its disclosure is opened and visible. Fetches never add text to future conversation uploads. |
|
|
455
|
+
| Retention | Observations belong to this page, not a durable history. Delivery changes and reconnects retain them; reload starts with only what the daemon supplies again. Do not reconstruct past ambient/outside events from messages, poll for them, persist them in browser storage or imply unseen activity was absent. Disconnection retains visibly stale evidence under {§web-session-observer}; neither events nor browser departure cancel or complete work. |
|
|
456
|
+
|
|
457
|
+
## §web-session-streams Observed executor outcomes
|
|
458
|
+
|
|
459
|
+
| Boundary | Contract |
|
|
460
|
+
|---|---|
|
|
461
|
+
| Boundary | The session page has no open-stream inventory or progress ledger. `status.streams` stays in client state; channel-progress updates create no document projection. Session lifecycle, `wait` and conversation receipts retain their own presentation. |
|
|
462
|
+
| Document | A native “Observed stream outcomes” disclosure lists conclusions received on this page. It is absent from presentation when empty. All supplied values are literal text; unchanged outcomes retain their disclosures and inspected output. |
|
|
463
|
+
| Outcomes | A `terminal_update` carrying `_meta.plurnk.result` is separately retained as a page-local observed outcome: resource, producer, supplied summary and Problem details. Result status and supplied exit code classify the entry, and a supplied exit code is shown; the full result is not dumped into markup. Deduplication remains in JavaScript. Identical repeated conclusions reuse the disclosure. Channel closure, command exit and session outcome are distinct. Reconnection retains observed outcomes; reload does not reconstruct them from conversation history. |
|
|
464
|
+
| Inspection | A native Read output control makes one explicit public `entry.read` management action (`_plurnk/action`) for that target and the page's immutable session. Acquire fresh same-origin credentials; declare no tools or control claim and upload no conversation. Validate the public `EntryReadResult`. Render every returned channel's exact text, state, MIME type and code-point length as a labelled output snapshot, never Markdown or executable content. The control remains available for an explicit refresh. |
|
|
465
|
+
| Failure and lifetime | Pending reads disable their own control. Failed reads preserve prior output, show the diagnostic and allow explicit retry; a disappeared row cannot receive a late response. Page departure aborts browser-owned reads, never execution. Disconnection retains visibly stale evidence under {§web-session-observer}. No automatic output fetching, polling, stream cancellation or browser persistence. |
|
|
466
|
+
| Separation | Inspected output and outcomes stay outside conversation messages and future prompt submissions. Ordinary tool results remain authoritative conversation evidence. A stream failure neither completes the session nor ends its observation. |
|
|
467
|
+
|
|
468
|
+
## §web-session-input Message composer and explicit Stop
|
|
469
|
+
|
|
470
|
+
| Boundary | Contract |
|
|
471
|
+
|---|---|
|
|
472
|
+
| Document | A document-owned OTM template places ⏹️ Stop, a native textarea named Message, and ▶️ Send in one row below status. The textarea starts at one line, grows with content to a bounded viewport-relative height, then scrolls internally; both buttons stretch to its height. Send uses Pico's primary palette and Stop its secondary tokens. The buttons submit independent native forms: Stop neither validates nor resets the message field. Icon-only buttons retain accessible names and native explanatory titles. JavaScript mounts them only on session pages; questions and command results stay in main, not the pinned composer. |
|
|
473
|
+
| Keyboard | Enter remains a newline. Ctrl+Enter in the message field requests native Send-form submission, preserving validation, slash-command routing and delivery availability. Composing text and repeated keydown events do not trigger the shortcut. The Send button exposes the shortcut in its title and accessible keyboard metadata. |
|
|
474
|
+
| Focus | The textarea retains a visible inward focus ring, keeping its visual boundary aligned with adjacent buttons as its content grows. Focus does not change the controls' geometry. |
|
|
475
|
+
| Send | Except for slash commands under {§web-session-commands}, submit exact nonblank text as one standard `session/prompt` request to `/acp`, on the document's qualified session. New text gets a new ID; a deliberate retry of the same unconfirmed draft retains its ID for daemon idempotency. Message identity generation works on ordinary HTTP as well as secure contexts; it does not change transport security or authentication policy. A prompt or resolution request declares an attended client and no unimplemented tools; passive connection alone never attaches control. Server permissions remain unchanged. |
|
|
476
|
+
| Handoff | One current ACP client owns the displayed messages. Close and await the preceding delivery before starting a prompt or resolution request. Held observation preserves the current interrupt set and the unsent review form; after settlement or explicit reconnection, passive reattachment starts fresh under {§web-session-observer}. Follow-ups during live work use the same ordinary message admission; no parallel transcript, injection endpoint, automatic retry or offline queue. |
|
|
477
|
+
| Admission | The daemon's `status.inbox`, user-message snapshot or user-message arrival confirms acceptance by message ID; local insertion never does. Acceptance precedes model observation: queued follow-ups remain discoverable during inference, after reconnect and after cancellation before observation. A prompt heads the turn that observes it; accepted-unobserved follow-ups remain at the tail of their loop ahead of later loops. Their eventual arrival retains the same ID and does not repeat text. |
|
|
478
|
+
| Draft | Prevent another submission until acceptance or explicit rediscovery after a failed delivery. Clear the submitted text only if the textarea still contains it, preserving newer typing. A refusal or uncertain connection failure retains the draft, explains uncertainty and requires explicit reconnection before another submission. Reattachment may confirm acceptance; absence is not proof of refusal, so a deliberate unchanged retry retains its idempotency key. No automatic resend, browser persistence or guarantee across full reloads is implied. |
|
|
479
|
+
| Stop | Explicit submission sends a standard `session/cancel` notification for the same session through an independent ACP connection, with no conversation messages. Its acknowledgement requests cancellation; only session status/terminal events establish the outcome. Stop preserves unsent text and targets no unrelated session. Show it only for known queued, running or parked work; unavailable delivery or an in-flight cancellation disables it without hiding known live work. A pending follow-up does not prevent stopping already-running work. |
|
|
480
|
+
| Interrupt | Retain the client's pending interrupts and call evidence. Proposals use the configured client choice under {§web-session-approval}; questions and MCP input use {§web-session-questions}. Input-required is not disconnection: keep observing authoritative work and pending-input changes through the public adapter contract. Unsupported interactions remain pending with an explicit notice. Send is unavailable until settlement; explicit Stop remains available for live work on this session. From the moment an interrupt arrives until it is presented, Send is unavailable: a message cannot overtake the review it would leave unanswered. |
|
|
481
|
+
| Failure and departure | Request failure is visible, never a successful send or cancellation. A session's own failure is its status; only a page that submitted the failed work also reports it as its request's. A Stop the page left before its answer says so on return. Refresh, navigation, page close and delivery handoff only release browser resources: they cannot cancel admitted work. No message or cancellation is automatically resubmitted after reconnect. |
|
|
482
|
+
|
|
483
|
+
## §web-session-commands Session-page slash commands
|
|
484
|
+
|
|
485
|
+
The first non-whitespace `/` selects a client command unless its name is advertised in `availableCommands` (from `session/resume` or `available_commands_update`), in which case it submits as an ordinary `session/prompt` under {§web-session-input}.
|
|
486
|
+
For client commands, the browser translates input to existing daemon actions; it owns no model,
|
|
487
|
+
capability, persistence or execution policy. Client commands require no inference.
|
|
488
|
+
|
|
489
|
+
| Input | Effect |
|
|
490
|
+
|---|---|
|
|
491
|
+
| `/help [command]` | Show this browser's supported commands or the named command's usage. Bare `/` shows the same index. |
|
|
492
|
+
| `/models [search]` | Inspect declared aliases and one bounded, optionally searched model-catalog page through `providers/list` and `models.list`. Preserve the supplied page counts; further search narrows the catalog. No endpoint probes. |
|
|
493
|
+
| `/model [selector] [effort]` | `session/resume` inspects the bound session's durable `configOptions`; `session/set_config_option` (`configId: "model"`) selects it, optionally setting effort (`configId: "thought_level"`). Display the confirmed route, not the submitted selector. |
|
|
494
|
+
| `/effort [policy]` | `session/resume` or `session/set_config_option` (`configId: "thought_level"`). Present the confirmed effort, provenance and supported efforts; no client-owned list of admitted policies. |
|
|
495
|
+
| `/capabilities [json]` | Inspect or replace the workspace policy through `workspace.capabilities.get` or `workspace.capabilities.set`. Present service, workspace and effective layers separately, preserving exact policy values. |
|
|
496
|
+
| `/yolo` | Toggle the current page's client approval preference under {§web-session-approval}. No daemon action, credential request or model message. |
|
|
497
|
+
| `/stop` | The same explicit cancellation path as the Stop form under {§web-session-input}; an idle session is not an error. |
|
|
498
|
+
|
|
499
|
+
| Boundary | Contract |
|
|
500
|
+
|---|---|
|
|
501
|
+
| Scope | Only the listed client commands; slash commands advertised in `availableCommands` pass through to `session/prompt`. Unknown unadvertised names and invalid argument shapes report a local diagnostic without inference or a substitute command. No terminal process, filesystem bridge, host-file imports/exports, external editor, command navigation or duplicate terminal framework. Workspace/subsystem management belongs to its own page surface. |
|
|
502
|
+
| Delivery | Each daemon action uses an independent official ACP client request on the document's immutable session, fresh credentials, empty messages and no tools or control claim. Preserve exact Problem details. Action results never replace the observer's client state or messages. |
|
|
503
|
+
| Presentation | Document-owned native templates render help, model tables, requested policy layers and confirmed outcomes. Failures show their diagnostic and recovery, not a duplicate serialized Problem. Do not add raw result inspectors. A separate page-local command-results list records each submitted command and its outcome; it is not conversation, durable history or browser storage. |
|
|
504
|
+
| Admission | Command intake is independent of model-message admission: pending reviews do not block commands. One ordinary command is pending at a time; the separate Stop form remains reachable. A successful command clears only its unchanged draft; failure retains it, and newer typing is never cleared. No automatic retries. |
|
|
505
|
+
| Arrival | A session page starts with the configured switch only when the person reached it by their own navigation: Fetch Metadata `Sec-Fetch-Site` is `same-origin` or `none`. Any other value starts it in manual review, so a link from another site never approves anything; so does no value, which is every arrival at a plain-HTTP origin other than localhost, where browsers send no Fetch Metadata and the person enables approval on the page. |
|
|
506
|
+
| Client approval | A YOLO change applies to outstanding proposals and future ones. Enabling it may submit an all-proposal pending set through the existing complete resume path. Questions and MCP input still require explicit answers; toggling preserves their unsent answers. Disabling cannot revoke an already-submitted decision. Reload restores the starting mode under the Arrival row. |
|
|
507
|
+
| Lifetime | Leaving the page aborts browser delivery, not an admitted mutation or agent work. An interrupted response is shown as unconfirmed, not left running if the browser restores the page. Commands and their results are never uploaded on later model messages. Command errors cannot end observation or fabricate successful model/effort changes. |
|
|
508
|
+
|
|
509
|
+
## §web-session-settings Native model and effort selection
|
|
510
|
+
|
|
511
|
+
| Boundary | Contract |
|
|
512
|
+
|---|---|
|
|
513
|
+
| Disclosure | Model and effort buttons reveal their respective inline articles after the conversation in main, never overlays. Native expanded/controls attributes identify the relationship. Opening focuses the form. Cancel and a confirmed selection return focus to the button before closing the article, preserving the status row under {§web-session-status}; a failed selection remains open. No separate Close control, visible heading, catalog counts or ordinary success narration. A missing effort still permits inspecting the daemon's supported choices. |
|
|
514
|
+
| Models | One native search input with a “Search Models” placeholder and accessible name, a `datalist` of suggestions, then Cancel / Select Model. Input queries the daemon's entire catalog using the same validated actions as `/models`; suggestions are a bounded result page, not a selectable-route allowlist. Search beyond the initial page by typing, or enter any exact alias/provider-model for daemon validation. Preserve supplied readiness in suggestion labels. Superseded reads cannot replace current suggestions or report stale failures. Search never selects a model; only explicit submission through `session/set_config_option` (`configId: "model"`) does. No provider probes, model-name guesses, inferred effort policies, client catalog or custom combobox. |
|
|
515
|
+
| Effort | One native select with disabled “Reasoning Effort” placeholder, then Cancel / Select Effort. Offer only the daemon's supported efforts from `session/resume` (`configOptions` for `thought_level`); submit through `session/set_config_option` (`configId: "thought_level"`). An empty set has no selectable policy and reports that fact. |
|
|
516
|
+
| Authority | Forms and slash commands share result validation and public management actions. Only confirmed mutations report success; refusal remains visible with its recovery and preserves the selection and message draft. Observer state remains authoritative for status. No conversation entries, inference, duplicate client policy or automatic retry. Leaving the page aborts delivery, not admitted work. |
|
|
517
|
+
|
|
518
|
+
## §web-session-approval Configured operation review
|
|
519
|
+
|
|
520
|
+
| Boundary | Contract |
|
|
521
|
+
|---|---|
|
|
522
|
+
| Status hover | The native toggle title describes the current mode as “Auto Approval (/yolo)” or “Manual Approval (/yolo)”. Its accessible toggle name stays stable while pressed state expresses enabledness. |
|
|
523
|
+
| Authority | The current ACP client's `pendingGates` supplies the complete presented set. `_meta.plurnk.proposal` supplies daemon-owned review facts. No client approval registry, permission rules, inferred ownership, or reconstruction from conversation calls. A delegated proposal need not have a `toolCallId` on its owner's session. |
|
|
524
|
+
| Configuration | `PLURNK_WEB_YOLO=1` automatically accepts approval proposals with decision `accept` and outcome `client_yolo`; `0`, the shipped default, requires manual review. Read at module startup through {§web-configuration} and project the resolved switch as the native status toggle's pressed state. That toggle and `/yolo` change the same preference for the current page under {§web-session-commands}; browser assets contain no policy default. This client choice cannot override daemon rejection and does not veto decisions by other authorized clients. |
|
|
525
|
+
| Document | Manual review uses one document-owned native form, a fieldset per operation, and required Accept/Reject radio choices with neither selected. Show the requesting session, operation, target and exact body, without a duplicate full-proposal dump. Review facts are literal text, never Markdown, executable controls or application bindings. |
|
|
526
|
+
| Decision | Submit the complete presented set by resolving the held `session/request_permission` and `elicitation/create` requests on `/acp`, retaining their opaque interrupt IDs and current session. Each proposal response carries `outcome: "selected"` with `optionId: "allow_once" | "reject_once"` and `_meta.plurnk.outcome`. An all-proposal YOLO set submits automatically. Questions and MCP input always require explicit answers under {§web-session-questions}; mixed sets combine those answers with the configured proposal decisions, never a partial submission. No new message, approval tool, synthetic tool result or body editing. The daemon owns validation, settlement and the operation's actual result. |
|
|
527
|
+
| Handoff | Disable review during submission, then follow the ordinary client conversation and outcomes. A subsequent gate applies the configured mode anew; manual choices start unselected. Otherwise return to passive observation. Keep unsent message text. Stop remains explicit cancellation of the page's session, not rejection of a proposal or cancellation of another requesting session. |
|
|
528
|
+
| Refresh and concurrency | Several authorized clients are ordinary views of one pending decision. The daemon settles once; all views withdraw obsolete controls and follow the resulting work without another answer or manual reconnection. Reconnection rediscovers current gates and fresh continuations. Never replay unsubmitted choices after refresh or retry a decision automatically. A stale, refused or uncertain submission is not acceptance; retain its diagnostic without obscuring independently confirmed progress. Actual delivery/credential failures follow {§web-session-observer}. |
|
|
529
|
+
| Deadline | A proposal whose interrupt carries `expiresAt` shows it as a native `<time>`; one that waits indefinitely shows none. The daemon cancels a proposal at its deadline, needing no client. Past that time by this page's clock, the page answers nothing, manually or automatically: the set is presented with a notice that the daemon cancels it, its controls unavailable, and the held observation follows the daemon's settlement. |
|
|
530
|
+
| Settled response | A resolution overtaken by another client's settlement or cancellation shows that the request is no longer pending, then refreshes current state without resending the answer. Other failures remain failures; a refusal does not acknowledge this client's decision. |
|
|
531
|
+
| Departure | Refresh, navigation, page close and lost delivery cannot reject a proposal, cancel work or undo an accepted decision. They release browser delivery only. |
|
|
532
|
+
| Unsupported sets | Every presented interrupt must be a review proposal or a question supported by {§web-session-questions}. Otherwise do not submit a partial set or guess a missing response contract. Explain that the interaction is unsupported and leave it pending. |
|
|
533
|
+
|
|
534
|
+
## §web-session-questions Schema-shaped answers
|
|
535
|
+
|
|
536
|
+
| Boundary | Contract |
|
|
537
|
+
|---|---|
|
|
538
|
+
| Authority | Questions and MCP input use the same standard gate resolution path as {§web-session-approval}. The client supplies the pending `elicitation/create` gate, its `requestedSchema` and `_meta.plurnk.interaction`; the browser invents no question endpoint, MCP continuation or interaction identity. |
|
|
539
|
+
| Attribution | When supplied, `_meta.plurnk.interaction.sessionId` identifies the requesting session in the question heading, including delegated questions and rediscovery. Never infer that identity from a tool-call or interrupt ID. Generic questions remain answerable without this metadata. |
|
|
540
|
+
| Document | Show the request message, tool name and exact call arguments as literal text. Keep the full response schema inspectable. Straightforward object properties with independent string, number, integer, boolean or enumerated values use native controls in document-owned templates. Other schemas remain whole in an explicitly labelled JSON editor; references, nesting and conditional rules are never flattened or partly discarded. |
|
|
541
|
+
| Values | Preserve strings verbatim and retain numeric, boolean and enum types. Optional fields have an explicit inclusion choice, so omission differs from an empty string, false or zero. Schema defaults may prefill an answer but cannot submit it. A required string may be empty unless the schema prohibits it. |
|
|
542
|
+
| Validation | Native controls enforce their own input constraints. The existing public contracts validator checks the complete proposed answer against the exact schema before sending; validation failure preserves editable input and shows the error. Invalid or unresolvable schemas remain visible and cancellable. Daemon validation and settlement remain authoritative. No schema reference initiates a browser network request. |
|
|
543
|
+
| Resolution | Explicit submission answers (`action: "accept"`, `content`) or cancels (`action: "cancel"`) every presented `elicitation/create` request, retaining opaque interrupt IDs. Cancellation of a question is not Stop and does not cancel unrelated work. Mixed proposal/question sets use the same complete-set rule. |
|
|
544
|
+
| Lifetime | Reuse {§web-session-approval}'s rediscovery, concurrent-client, uncertainty and departure rules. A failed or uncertain submission is not acceptance and is never retried automatically. Refresh does not preserve unsubmitted answers; correcting local validation does. |
|
|
545
|
+
| Security | No request, schema title, field name or answer becomes HTML, an application binding, a navigation instruction or executable code. Keep user data in memory/controls only, not browser storage. External URLs in requests are text, never automatically opened. |
|
|
546
|
+
|
|
547
|
+
## §web-session-conversation Conversation projection
|
|
548
|
+
|
|
549
|
+
| Boundary | Contract |
|
|
550
|
+
|---|---|
|
|
551
|
+
| Authority | The session ACP client's ordered `messages` collection is the sole live transcript. Read-only daemon history pages join its display under {§web-session-history}, never its collection or request input. No custom-row reconstruction, browser persistence or second event reducer. Observation submits no input and never answers a tool call; authored input follows {§web-session-input}. |
|
|
552
|
+
| Identity | Project message content and each assistant call in supplied chronology, with archival joins under {§web-session-history}, then supplied call order, grouped by loop (`_meta.plurnk.loopSeq` or coordinate prefix, defaulting to `1`) into initially open loop disclosures (`Loop <sequence>`). Within a loop, user prompts, assistant/system/developer prose, and `send`, `done` and `fail` replies are non-folding article entries; a `done` or `fail` whose `repliedTo` resolves a mid-loop prompt (any prompt after the loop's opening prompt) nests inside that prompt's article, while `send` and loop-level completions remain at the loop level. Contiguous runs of working operations (non-reply calls, results and reasoning) between loop-level entries group into an initially closed operations disclosure whose summary tallies each operation kind in appearance order (`[glyph]/op # · ...`). When the latest loop has not concluded with a loop-level `done` or `fail` and its latest unretired `todo` (`plan_update` or `todo` body) has open items, a non-folding `Open tasks` article (`data-loop-plan`) at the tail of the loop lists only those pending items (`☐`), and is removed when no open items remain, `plan_removed` or `kill` retires the plan, or the loop concludes. Content precedes calls within a message; a call-only message has no empty enclosing article. Each entry retains its message ID and, for calls, call ID. Snapshots replace live membership and order; streaming changes update existing entries. Unchanged bodies, selection and native disclosure state survive updates and passive reconnections. Rendered-node lookup comes from the DOM; the renderer's per-outlet last-render cache avoids replacing unchanged content, but holds no separate message collection. A page reload reads fresh authority state, not browser history. |
|
|
553
|
+
| User and assistant | Content-first semantic articles, with accessible names identifying role and supplied author/subagent attribution, without redundant visible headers. Text is rendered as Markdown. Calls retain their containing message's identity and attribution without duplicating its envelope in the DOM. Empty content does not imply a fabricated answer. |
|
|
554
|
+
| Reasoning | Apply {§web-session-reasoning}. Nonempty standard reasoning content is literal preformatted text in a collapsed disclosure; negotiated empty messages have no visible row until their body reference arrives. |
|
|
555
|
+
| Calls and results | Confirmed Plurnk operations use {§web-operation-summary}. Their immediate result (correlated by call ID, without its own Plurnk row metadata) completes the call's existing entry rather than creating another article. Results with their own row metadata remain distinct observations or replies; they never inherit the invocation's operands. Generic calls/results retain supplied chronology and accessible identity without guessing a Plurnk contract; late archival receipts join under {§web-session-history}. Standard tool errors remain visible. |
|
|
556
|
+
| Working notes | `note` calls follow {§web-working-notes}; they remain calls, not answers or provider reasoning. |
|
|
557
|
+
| Delivered replies | `send`, `done` and `fail` bodies use the same sanitized Markdown renderer as assistant prose. A tool result attributed to a `send`, `done` or `fail` row is the delivered reply itself, not an operation receipt; render its complete content without unwrapping JSON-shaped replies as metadata. Other authored operation bodies remain literal preformatted text. |
|
|
558
|
+
| Other messages | System/developer messages use the same accessible article boundary. Text and supported document parts retain their original order. Unmapped structured payloads stay in JavaScript; do not serialize them into the document. No inline media player or attachment service. Supplied subagent invocation IDs identify attribution, never mistaken for the parent's output. |
|
|
559
|
+
| Semantics | Entry attributes carry only identity, role, attribution, operation/category and result state relevant to presentation or interaction. Text lives in content nodes, not duplicate data attributes. Structured protocol metadata remains in JavaScript, not attributes or generic JSON disclosures. Accessible naming is independent of visible labels and decorative glyphs. Native structure and controls supply meaning and behavior; CSS alone owns arrangement and decoration. An empty body stays empty; do not invent visible labels or controls to fill it. |
|
|
560
|
+
| Layout | Native unnumbered `ul` lists, loop and operation-run disclosures, and content articles, preserving client order and authored Markdown list semantics, with card body disclosure under {§web-card-disclosure}. No wrapper solely to reproduce protocol nesting, duplicate session heading, chat bubbles, application classes or virtual-DOM framework. Literal content wraps under {§web-content-reflow}; explicit code, tables, formulas and diagrams scroll locally when too wide. |
|
|
561
|
+
| Connection | The existing connection notice governs whether displayed history is live. Only a supplied empty message collection says “No messages.” Unknown history and deliberately unrendered reasoning placeholders do not imply emptiness. A transport failure retains the last observed projection, never an invented empty or successfully completed conversation. |
|
|
562
|
+
|
|
563
|
+
## §web-card-disclosure Card bodies
|
|
564
|
+
|
|
565
|
+
| Boundary | Contract |
|
|
566
|
+
|---|---|
|
|
567
|
+
| Order | The always-visible header or summary contains the heading (decorative glyph and authored aside), optional diagnostic, then optional operation operands/counts. Omit absent regions. A diagnostic never replaces authored intent. The body follows these regions. |
|
|
568
|
+
| Interaction | Working operations, observed activity and diagnostics with a nonempty body use native `details/summary`; the existing summary is its control, without a synthetic “View body” label. With no body there is no disclosure. User prompts, assistant/system/developer prose, and `send`, `done` and `fail` replies render directly in their article without an individual card disclosure. Summary and header fields use phrasing content; CSS arranges their rows. Accessible names identify the card or operation-run tally independently of glyphs and include visible diagnostics. |
|
|
569
|
+
| Spacing | Retain Pico card padding and spacing between body paragraphs, without an extra trailing disclosure/content margin or an empty body strip below a header-only card. Multiline summaries retain ordinary line spacing; their chevron stays beside the first row, not on an additional empty row. |
|
|
570
|
+
| Defaults | Loop disclosures start open. Contiguous operation-run disclosures and working-operation card disclosures (including `note`, `edit`, ordinary tool evidence, observed activity and completed reasoning) start closed. Each disclosure is independent, never an exclusive accordion. Live reasoning retains its visible bounded preview under {§web-session-reasoning}. |
|
|
571
|
+
| Continuity | Set the initial open state only when creating a disclosure. Surviving disclosures retain the reader's choice across content updates, receipt arrival, paging, replay and reconnect. Preserve unchanged body nodes and selection. No click controller, preference store or body copy in attributes. |
|
|
572
|
+
| Boundaries | Disclosure changes visibility, not content policy or lifecycle. Literal text and sanitized Markdown keep their existing owners. Referenced bodies load only when visible under {§web-result-bodies}; operation summaries do not gain `read` receipt readback. |
|
|
573
|
+
|
|
574
|
+
## §web-conversation-surfaces Conversation backgrounds
|
|
575
|
+
|
|
576
|
+
| Entry | Surface |
|
|
577
|
+
|---|---|
|
|
578
|
+
| User content, including incoming messages annotated as `send`, `done` or `fail` | Distinguishable green tint derived from the active Pico theme with an explicit tinted border; user authorship takes precedence over operation metadata. |
|
|
579
|
+
| Assistant prose and confirmed `send`, `done` and `fail` bodies, including delivered tool-role replies | Ordinary Pico card surface. |
|
|
580
|
+
| Other conversation entries, including assistant tool calls, `note` and reasoning; live reasoning preview | Neutral muted slate sectioning surface derived from Pico's theme tokens. |
|
|
581
|
+
|
|
582
|
+
Existing role and operation attributes select the category in CSS; no additional
|
|
583
|
+
markup, presentation field or JavaScript classification. An operation's header
|
|
584
|
+
shares its body's background. Normal text and links retain at least 4.5:1 contrast
|
|
585
|
+
in light and dark modes. Accessible identity and content remain independent of
|
|
586
|
+
color. Non-conversation cards retain their existing Pico presentation.
|
|
587
|
+
## §web-operation-summary Shared operation summary
|
|
588
|
+
|
|
589
|
+
| Boundary | Contract |
|
|
590
|
+
|---|---|
|
|
591
|
+
| Structure | One article follows {§web-card-disclosure}: glyph and authored aside, optional diagnostic, then supplied operands and returned count, all visible independently of its body. Omit absent fields and empty rows. These are semantic rows, not fixed-width lines; long content may wrap. Runtime names remain visible beside their generic glyph. |
|
|
592
|
+
| Identity | An accessible article name identifies the operation and attribution independently of CSS. Glyphs are CSS-generated from operation/signal attributes, never body text or HTML icons. Authored asides stay visible beside the glyph. |
|
|
593
|
+
| Operands | Use the adapter's authored `target.raw`, `lineMarker.marks` and `matcher.raw`. `copy`/`move` preserve source/destination order and each operand's own scope/pattern. Without an authored target, the operation's own receipt path or row address may identify its resource. Never parse a heading, substitute a causal source or borrow another row's operands. Semantic field spans contain literal text, not duplicate field values in attributes. |
|
|
594
|
+
| Result | `read` counts supplied `lineOrdinals`, otherwise its inclusive returned range; `find` counts its inclusive returned range, otherwise its supplied total. Do not count or fetch body text to infer a result size. Show supplied failure title/detail and recovery, standard tool errors, and a no-content diagnostic when supplied; do not print successful status numbers or receipt JSON. |
|
|
595
|
+
| Content | Authored bodies retain {§web-working-notes} and {§web-conversation-content}, with disclosure under {§web-card-disclosure}. The shared summary does not display or fetch receipt bodies, including `read` results. Operation-specific readback and previews are separate behavior, not inferred from this summary. |
|
|
596
|
+
| Continuity | Derive the summary from the live collection and read-only pages under {§web-session-history}. Receipt arrival updates summary fields without replacing an unchanged authored body or its selection. Later observations keep their own identity and result state. |
|
|
597
|
+
|
|
598
|
+
## §web-session-history Read-only conversation pages
|
|
599
|
+
|
|
600
|
+
| Boundary | Contract |
|
|
601
|
+
|---|---|
|
|
602
|
+
| Authority | `_meta.plurnk.history.older` on the `session/resume` response is an opaque URL or null. Follow it unchanged for `{final, updates, older}` (projected via `SessionTranslator.projectMessages`). The daemon owns loop membership, cuts and overflow; web imposes no second count/size policy and derives no cursor from message IDs or coordinates. Standard schemas validate pages. |
|
|
603
|
+
| Loading | A native **Load previous loop** control marks each unloaded boundary, ordinarily at the top. Fetch only on activation, using the same origin, fresh credential, no-store and redirect refusal as body readback. One pending read per URL; failures retain the view and show a concise retryable diagnostic at that boundary. No speculative prefetch or automatic failure retry. |
|
|
604
|
+
| Projection | Retain explicitly loaded pages for the page visit. Render their supplied chronology with the live window, once per message identity; current live values win overlap. A late immediate result completes its archived call in place. Never append fetched pages, references or preserved reading material to the ACP client, incoming notification streams or prompt submissions. |
|
|
605
|
+
| Advancement | A replacement snapshot owns the live window. Explicitly loaded pages remain. An unloaded gap between explicitly loaded pages and the new window has its own history control: do not imply continuity or silently fetch intervening history. |
|
|
606
|
+
| Freshness | `final: true` pages are immutable for this visit. Revalidate loaded non-final pages on a new snapshot, preserving live precedence during the read. Failed revalidation remains visible with explicit retry. In-flight reads cannot resurrect a discarded boundary or publish after page departure. A reload starts from a fresh daemon window. |
|
|
607
|
+
| Reading | Prepending pages preserves unchanged nodes, selection and disclosure state. The native main scroll region is the only page scroller; the browser owns scrolling and scroll anchoring, and stream updates never move focus. A review, message feedback and command results follow the conversation at main's end. No JavaScript scroll controller, virtualized rows, timed scrolling or browser history persistence. |
|
|
608
|
+
|
|
609
|
+
## §web-result-bodies Results by reference
|
|
610
|
+
|
|
611
|
+
| Boundary | Contract |
|
|
612
|
+
|---|---|
|
|
613
|
+
| Intake | Standard `resource_link` content blocks (`type: "resource_link"`, `uri`, `mimeType`) provide content for one preformatted outlet where that surface displays bodies. Operation summaries under {§web-operation-summary} do not mount receipt-body outlets. Do not display the transport URL, MIME inventory or a Read body inspector. Reference locations remain in JavaScript; the client's parts remain unchanged. |
|
|
614
|
+
| Fetch | Native intersection observation reads a body once its outlet enters the viewport. Off-screen bodies and closed disclosures do not fetch. Require a same-origin HTTP(S) URL without embedded credentials; reject redirects. Use a fresh session access credential through {§web-access-token}, or none for explicitly anonymous documents. Never put a bearer in a URL, forward it to another origin, substitute a guessed URL or start an ACP request. |
|
|
615
|
+
| Content | Show fetched bytes decoded as text in a literal preformatted outlet, even for HTML, SVG or Markdown. A body cannot create controls, application bindings, scripts or further requests. This read-only inspection is not an attachment editor or a second Markdown rendering policy. |
|
|
616
|
+
| Failure | The outlet announces pending state through `aria-busy`. A failed read shows its diagnostic (the daemon's Problem detail when it supplies one) and a native Retry button in place of content. A read the page abandoned, as when it is left, is no failure: it rests empty with a native Read button. No automatic retry, duplicate pending fetch or silent empty success. |
|
|
617
|
+
| Lifetime | Fetched text belongs only to its existing DOM outlet. Unchanged replay preserves it; replacing/removing the content invalidates late results. It never enters client messages, subsequent prompt submissions, browser storage or a separate cache. Page departure aborts these browser-owned fetches, not daemon work. |
|
|
618
|
+
|
|
619
|
+
## §web-session-reasoning Ephemeral preview, durable reference
|
|
620
|
+
|
|
621
|
+
| Boundary | Contract |
|
|
622
|
+
|---|---|
|
|
623
|
+
| Negotiation | Every browser ACP request supplies `_meta.plurnk.reasoning: "link"`, including observation, held observation, prompt submission, resume and management requests. Preserve other request properties. The ACP client keeps the standard empty reasoning messages and their metadata; web never writes preview or fetched text into them. Unnegotiated third-party clients retain the daemon's standard reasoning behavior. |
|
|
624
|
+
| Preview | `agent_thought_chunk` session updates feed one literal-text article outside the message list, with a 💭 header and `<sessionName> > Loop <loop> > Turn <turn>` (or `Loop <loop> > Turn <turn>` when `sessionName` is absent) from `_meta.plurnk` when present. A new attempt replaces its predecessor. A delta must name that active message. No Markdown interpretation, transcript entry, hidden full-trace accumulator or browser persistence. |
|
|
625
|
+
| Retention | `PLURNK_WEB_REASONING_PREVIEW_CHARS` is a positive integer read at module startup and projected explicitly into session documents. Retain only the newest configured number of Unicode code points across fragments. CSS bounds the visible area to one third of the dynamic viewport. Streaming updates do not move focus or announce each token. |
|
|
626
|
+
| Clearing | The matching reasoning-message end clears its preview. Turn start, terminal outcome, delivery failure, delivery handoff and page departure clear it too. Reattachment never reconstructs a preview from snapshots or body links. A link-only repeated start/end cannot clear another attempt's preview. No preview is retained as evidence of a failed request. |
|
|
627
|
+
| History | A reasoning message's `_meta.plurnk.body` supplies its exact URL and MIME type, including when metadata arrives after an empty message. An article contains its initially collapsed native disclosure, headed by a decorative 💭 and `<sessionName> > Loop <loop> > Turn <turn>` (or `Loop <loop> > Turn <turn>` when `sessionName` is absent) from `_meta.plurnk` when available; its accessible name identifies reasoning independently of the glyph. Message IDs and reference URLs are opaque, never sources of coordinates. The disclosure defers the read until opened and visible under {§web-result-bodies}. No second Read body control. Identity, order, attribution and unchanged loaded outlets survive ordinary snapshots. |
|
|
628
|
+
| Ownership | The preview is presentation only. Its clearing does not cancel inference or erase daemon evidence. Full reasoning is read-only, literal on-demand inspection; neither reading nor streaming contributes text to subsequent client requests. |
|
|
629
|
+
|
|
630
|
+
## §web-conversation-content Untrusted message content
|
|
631
|
+
|
|
632
|
+
| Boundary | Contract |
|
|
633
|
+
|---|---|
|
|
634
|
+
| Markdown | Marked parses ordinary text, code, links, lists and tables. Raw HTML is escaped. Authored images become links rather than automatic network requests. Tables have native figure containers. Mermaid follows {§web-diagram-content}; no syntax highlighter or embedded application is implied. Malformed fences follow the parser's ordinary Markdown semantics, without operation-style recovery or inferred intent. Math follows {§web-math-content}; footnotes follow {§web-footnote-content}. |
|
|
635
|
+
| DOM admission | DOMPurify sanitizes the generated HTML into a fragment before insertion. Only Markdown presentation elements and their necessary attributes are admitted; no application data attributes, authored IDs, styles, scripts, forms, frames, SVG or active controls. Generated footnote anchors and accessible labels are presentation, not permission to interpret authored HTML. GFM task checkboxes remain disabled. Links use the sanitizer's URI checks; no model-supplied HTML bypasses sanitization. |
|
|
636
|
+
| Literal content | `note`, reasoning and ordinary operation evidence enter template-owned `pre` elements as text nodes, never Markdown or HTML. Whitespace, entities and tag-shaped strings stay literal; content cannot close its container or create elements. `pre` supplies semantics, not sanitization. No escaping-and-reparsing filter or body copy in attributes. |
|
|
637
|
+
| Ownership | Markdown owns only its content outlet. OTM owns message structure and scoped application state; rendered content cannot become an OTM binding or mutate navigation, credentials or controls. Sanitized fragments are inserted directly; only recognized math and diagram outlets receive their bounded enhancements. |
|
|
638
|
+
| Continuity | The same admission boundary applies to every rich-text message and `send` body, including partial updates and replay. Message identities, author/category labels, call arguments, errors and structured payloads remain data, never markup. Rendering performs no model-directed navigation or resource request; ordinary links require user activation. |
|
|
639
|
+
| Build | Parser and sanitizer are bundled only with the session entry and include their licenses. The allowed Markdown elements participate in CSS pruning so production preserves their Pico presentation. Math code and its vendor stylesheet load only when a recognized formula occurs; fonts are requested by the renderer's CSS when needed. Only recognized Mermaid blocks request server compilation. Renderer-owned selectors survive pruning. All assets and licenses are locally packaged. |
|
|
640
|
+
|
|
641
|
+
## §web-working-notes Inspectable working material
|
|
642
|
+
|
|
643
|
+
A standard call whose `name` and `_meta.plurnk.op` are both `note`
|
|
644
|
+
presents its string `body` as literal text in a `pre` inside one direct list entry's
|
|
645
|
+
article. No Markdown interpretation or `code` semantics are implied.
|
|
646
|
+
`data-plurnk-type="note"` identifies the operation, and any supplied `aside`
|
|
647
|
+
renders in the shared operation header under {§web-operation-summary}. Synthesized
|
|
648
|
+
`kill` distillation `note` rows carrying `_meta.plurnk.attrs.distilled` are
|
|
649
|
+
suppressed so `kill` owns its authored distillation body without a duplicate row.
|
|
650
|
+
No enclosing assistant article, nested call list or arguments copy is added. Its
|
|
651
|
+
literal body uses the initially collapsed disclosure under {§web-card-disclosure}.
|
|
652
|
+
Incomplete JSON, non-object arguments or non-string bodies supply no body;
|
|
653
|
+
they are not repaired or printed as protocol envelopes. An absent or non-string
|
|
654
|
+
`aside` cannot erase a valid body.
|
|
655
|
+
|
|
656
|
+
The message identity, call identity and attribution remain unchanged. The
|
|
657
|
+
correlated immediate receipt contributes to {§web-operation-summary}, not a second
|
|
658
|
+
body. Changes to argument shape preserve the call entry; unchanged bodies retain
|
|
659
|
+
their nodes and selection.
|
|
660
|
+
Generic tool calls and results do not acquire Markdown interpretation from their
|
|
661
|
+
name or content. This presentation adds no conversation store or protocol role.
|
|
662
|
+
|
|
663
|
+
## §web-footnote-content Message-local footnotes
|
|
664
|
+
|
|
665
|
+
| Boundary | Contract |
|
|
666
|
+
|---|---|
|
|
667
|
+
| Interpretation | The locally bundled `marked-footnote` extension parses references and definitions. Marked's public renderers supply presentation; no parser fork, token rewriting or post-render anchor repair. Undefined references and incomplete syntax follow the library and remain readable. Code retains ordinary Markdown boundaries. |
|
|
668
|
+
| Presentation | Superscript reference links lead to a native ordered list labelled Footnotes. Each definition has a back-link for every occurrence, with accessible reference labels. Fragment navigation is native; no scrolling handler or application action. |
|
|
669
|
+
| Identity | Each rich message body and `send` call has its own anchor namespace, derived from its SDK identity. Definition and occurrence coordinates come from the extension, never arbitrary source-label concatenation. Distinct outlets cannot collide, including when a message and a call have the same ID. Identical content and identity reproduce identical anchors after streaming or replay; no persistent footnote registry or cross-message definition lookup. |
|
|
670
|
+
| Admission | Definition bodies pass through the same Markdown renderer and sanitizer as ordinary prose, including nested Markdown and math. Raw HTML remains text. Footnote labels cannot create IDs, controls or bindings. `note`, reasoning and ordinary operation evidence remain literal. |
|
|
671
|
+
| Continuity | Parsing state is confined to one body render. Partial updates cannot borrow definitions from earlier renders or other bodies. Unchanged bodies preserve their nodes, selection and native disclosure state under {§web-session-conversation}. Reflow follows {§web-content-reflow}. |
|
|
672
|
+
|
|
673
|
+
## §web-math-content Native mathematics
|
|
674
|
+
|
|
675
|
+
| Boundary | Contract |
|
|
676
|
+
|---|---|
|
|
677
|
+
| Delimiters | Marked extensions recognize `$…$`, `$$…$$`, `\(…\)` and `\[…\]` outside inline/fenced/indented code. Single-dollar math stays on one line, has no whitespace immediately inside either delimiter, and cannot end immediately before a digit; ordinary currency such as `$50 and $100` remains text. Escaped delimiters remain literal. Block forms can span lines. |
|
|
678
|
+
| Interpretation | Temml's public DOM renderer produces native MathML, with an annotation retaining the TeX source. No TeX document engine, cross-expression macro state, global equation numbering or browser-specific renderer. |
|
|
679
|
+
| Admission | Untrusted mode rejects TeX HTML/media/link extensions. DOMPurify sanitizes renderer-owned DOM in place, without serializing styles back through HTML. Renderer-required classes and layout styles remain inside this content boundary; IDs, navigation attributes and application bindings do not. The Markdown allowlist is not widened to accept authored MathML or HTML. |
|
|
680
|
+
| Failure and streaming | Incomplete delimiters stay text. Unsupported/malformed complete formulas retain their exact delimited source with a concise local error. The source remains readable while the renderer loads or if loading fails. Detached outlets cannot overwrite a newer message. Unchanged bodies retain their existing MathML and native selection. Unexpected implementation failures remain diagnostics, not concealed parse errors. |
|
|
681
|
+
|
|
682
|
+
## §web-diagram-content Static diagram images
|
|
683
|
+
|
|
684
|
+
| Boundary | Contract |
|
|
685
|
+
|---|---|
|
|
686
|
+
| Recognition | Marked code blocks whose first language word is `mermaid` (case-insensitive) are eligible. Inline code, indented code and nested fenced examples retain ordinary Markdown meaning. No inferred fence repair, orientation rewrite or alternate parser. |
|
|
687
|
+
| Interpretation | The web module compiles source through Ondine's public Node compiler under {§web-diagram-service}. Supported syntax belongs to the pinned compiler, not a second parser or a promise of complete Mermaid conformance. Preserve authored direction and intrinsic dimensions; use standalone appearance, not an application theme system. No browser compiler, DOM-dependent runtime or headless browser. |
|
|
688
|
+
| Image boundary | Generated SVG becomes a data-URI source of a native `img` in a document-owned OTM template. Never insert SVG markup into the application DOM or an embedded document. Native SVG image restrictions prevent scripts and external resource loading, including renderer font imports; the application CSP is unchanged. Authored Markdown images and raw SVG receive no new permission. |
|
|
689
|
+
| Source and failure | Source remains readable while compilation is pending. A successful image has an accessible label and an adjacent native source disclosure. Unsupported syntax, HTTP or rendering/decoding failure retains the source with a concise local diagnostic and the returned diagnostic or original error available as its title. A page compiles one diagram at a time, so any number of diagrams stays within {§web-diagram-service}'s admission. A refusal for capacity or time (429, 408, 503) or a lost request also offers a native Retry button that queues the same source again; other failures are final. No rewriting, alternate renderer or automatic retry. |
|
|
690
|
+
| Continuity | Detached outlets cannot publish a late image into a newer message. Unchanged bodies retain their image and native source-disclosure state. A rendered diagram is static presentation, not an interactive link or control; the image/source boundary applies to every rich-text message and `send` body. `note` and reasoning cannot initiate diagram compilation. |
|
|
691
|
+
| Viewport | Keep intrinsic diagram dimensions and browser zoom. Wide images scroll inside their own viewport without shrinking labels or widening the whole page; source remains selectable text. |
|
|
692
|
+
|
|
693
|
+
Reference: [SVG image restrictions](https://developer.mozilla.org/en-US/docs/Web/SVG/Guides/SVG_as_an_image#restrictions).
|
|
694
|
+
|
|
695
|
+
## §web-diagram-service Diagram compilation boundary
|
|
696
|
+
|
|
697
|
+
| Boundary | Contract |
|
|
698
|
+
|---|---|
|
|
699
|
+
| HTTP | `POST /render/mermaid` accepts UTF-8 `application/json` containing one `source` string and returns standalone `image/svg+xml`. Other methods return 405 with `Allow: POST`; other media types return 415; invalid UTF-8, JSON or shape returns 400. No CORS admission, simple form submission, external source URL or filesystem path. Browser fetch metadata must be same-origin when present. |
|
|
700
|
+
| Admission | With daemon authentication enabled, require the existing valid web session, otherwise 401 without a login redirect. Explicitly disabled daemon authentication admits anonymous compilation. This is presentation, not an agent operation or an ACP extension. |
|
|
701
|
+
| Isolation | Ondine executes in a bounded Node thread pool, never on the daemon event loop. The environment owns concurrency, admitted render count, queue-inclusive execution deadline, request-byte limit, compiler edge limit and SVG-byte limit. Request-body transport deadlines remain the HTTP host's concern. Compiler limits and thread termination are not an OS memory sandbox. |
|
|
702
|
+
| Failure | Oversized input returns 413; unsupported or invalid diagrams and compiler/output limits return 422; full admission returns 429; execution deadline returns 408; module shutdown returns 503. Failures are Problems, not images. Preserve unexpected compiler failures for the host's normal error path. |
|
|
703
|
+
| Lifetime | Start compiler threads only on demand. Expired or disconnected render requests stop their pending or active compilation. Module close rejects outstanding compilation and awaits thread termination. Neither browser departure nor renderer failure changes agent work. No persisted render cache or temporary files. |
|
|
704
|
+
| Output | Responses are private and not cached. SVG has `nosniff` and a sandboxed CSP denying scripts and external resource loads, including when opened as a document. The browser consumes it only through {§web-diagram-content}, without changing the application's CSP. |
|
|
705
|
+
|
|
706
|
+
## §web-content-reflow Constrained and changing viewports
|
|
707
|
+
|
|
708
|
+
Ordinary prose, literal working material and controls reflow at 320 CSS pixels
|
|
709
|
+
without page-wide horizontal scrolling. Literal text preserves authored newlines,
|
|
710
|
+
indentation and source text while wrapping long lines and unbroken tokens; visual
|
|
711
|
+
wraps add no hard line breaks. Native selection is not intercepted. Settled entries
|
|
712
|
+
grow vertically without an internal vertical scroll area;
|
|
713
|
+
the bounded live preview remains governed by {§web-session-reasoning}.
|
|
714
|
+
Explicit code, tables, formulas and diagrams that intrinsically require two
|
|
715
|
+
dimensions scroll within their own content boundary, not the whole message.
|
|
716
|
+
Raw tool output is literal working material, not inferred code or a table.
|
|
717
|
+
Keep readable font sizes and native zoom; do not shrink content or rewrite diagram orientation.
|
|
718
|
+
Native CSS responds to viewport changes without a JavaScript layout or scroll
|
|
719
|
+
controller. Browser rendering
|
|
720
|
+
limitations do not introduce a second renderer or compatibility polyfill.
|
|
721
|
+
|
|
722
|
+
## §web-page-shell Native page shell
|
|
723
|
+
|
|
724
|
+
| Surface | Contract |
|
|
725
|
+
|---|---|
|
|
726
|
+
| Shared navigation | Home/brand and the Workspaces breadcrumb root are real links preceding the single `main` landmark (the workspace index instead marks its root as current); no skip-navigation control. Authenticated documents add Account and a compact secondary Sign Out POST form in the header; no footer logout. Anonymous documents omit account/session controls. Native links and controls retain visible keyboard focus. |
|
|
727
|
+
| Header actions | Mascot-only home link with accessible Plurnk name; its square SVG uses the two-character monospace scale (`2ch`) of the navigation emoji, rather than a fixed pixel size. ⚙️ Account, ✖ Sign out: icon-only controls with explicit accessible names and native explanatory titles. Brand and actions align in one horizontal row, including narrow viewports. The workspace root is 🗃️ with an explicit Workspaces accessible name/title, not another menu action; workspace and session names remain text. |
|
|
728
|
+
| Breadcrumb layout | A compact single-row CSS grid keeps the breadcrumb between mascot and menu. Complete names never wrap or abbreviate; overflow scrolls horizontally within the breadcrumb, not the document. Pico's navigation padding belongs inside the breadcrumb's scroll area, with no vertical overflow or clipped content; its keyboard-focus ring stays inward. Native keyboard focus reveals linked ancestors without JavaScript measurements or a responsive navigation widget. The header adds no further block padding. Keyboard order remains mascot, menu, breadcrumb. |
|
|
729
|
+
| Disconnection | A decorative 🔌 with an accessible Disconnected name/title appears immediately left of Account when the session or overview observation is disconnected or signed out. It is absent during initial connection, healthy observation and ordinary input interrupts, and on documents without live observation. CSS projects the existing connection state; no browser-online inference, duplicate state or new action. Existing failure details and recovery controls remain available. |
|
|
730
|
+
| Document title | Home: `Plurnk`. Directory/error: its identity or HTTP status followed by ` — Plurnk`. Home and directory pages omit duplicate headings; the breadcrumb identifies the current session. Account and error pages retain one `h1`. |
|
|
731
|
+
| Directory breadcrumbs | 🗃️ → whole workspace name → session; only applicable ancestors appear. Non-directory pages retain the root link, including Account/session navigation. The current item is text marked `aria-current="page"`. The workspace-page marker follows {§web-workspace-folder}. Slashes in names do not invent intermediate directories. |
|
|
732
|
+
| Empty lists | “No workspaces.” or “No sessions.”; no empty list masquerading as content. |
|
|
733
|
+
| Page width | Account and error documents retain a centered 65ch reading measure. Home, Workspaces, Workspace and Session share the available viewport width. Gutters and wrapping preserve narrow-screen usability and complete names. |
|
|
734
|
+
| Viewport layout | Home and directory/session documents use a viewport-height CSS grid: stationary header, scrollable main, and (on sessions) stationary status/composer footer. Native viewport resizing and textarea sizing handle changing available space; no measured offsets or JavaScript layout controller. Main retains conversation, settings, reviews, detailed connection/message feedback and command results. Reading/form documents retain ordinary page flow. |
|
|
735
|
+
| Directory entries | Native lists use `li > article`. Workspace headers retain folder/cloud identity; session cards use the identity/disclosure under {§web-session-directory}. Accessible names do not depend on CSS glyphs. Preserve nested child disclosures and the daemon's membership/order; no second browser directory or custom tree widget. |
|
|
736
|
+
| Expected HTTP failures | A Pico `article` card within the same HTML shell: heading, decorative warning emoji, escaped explanation and native Home link. Preserve status/Allow/HEAD semantics and private headers from {§web-directory-documents}. Do not echo URLs, credentials or internal exceptions. Unknown internal failures still propagate to the host. |
|
|
737
|
+
|
|
738
|
+
The compiled root document owns shared navigation and assets, including an inert
|
|
739
|
+
template for authenticated navigation. OTM's documented indirect-template marker
|
|
740
|
+
identifies its server-owned instantiation. Server rendering selects that template
|
|
741
|
+
and supplies the title, header breadcrumb and main content. Ordinary navigation, focus, lists and forms work without JavaScript;
|
|
742
|
+
there is no browser-owned route or session inventory.
|
|
743
|
+
|
|
744
|
+
## §web-document Document
|
|
745
|
+
|
|
746
|
+
The unauthenticated home document contains only the shared navigation and an empty main landmark;
|
|
747
|
+
admitted home renders the daemon-wide session overview. The document initializes OTM from locally packaged ES modules. Locally packaged Pico Classless Green styles semantic elements
|
|
748
|
+
without application classes. Brand green is `#148800`; dark-mode link colors remain
|
|
749
|
+
bright enough for contrast. Native color-scheme selection is preserved; typography
|
|
750
|
+
uses Pico's system monospace stack and a shared numeric line-height token of 1.25.
|
|
751
|
+
Content and styling
|
|
752
|
+
work without JavaScript.
|
|
753
|
+
There is no theme switcher, client routing, persistent browser storage, external
|
|
754
|
+
asset request. The root links to the directory index. Protected
|
|
755
|
+
directory documents supply daemon identities as escaped HTML, never credentials.
|
|
756
|
+
|
|
757
|
+
Source stylesheets load in this order, with one concern per file. Production
|
|
758
|
+
compilation may combine them without changing their cascade:
|
|
759
|
+
|
|
760
|
+
| File | Responsibility |
|
|
761
|
+
|---|---|
|
|
762
|
+
| `app-vendor.css` | Unmodified pinned Pico Classless Green, copied during build. |
|
|
763
|
+
| `app-structure.css` | Readable page width, gutters and wrapping. |
|
|
764
|
+
| `app-style.css` | Presentation through substrate tokens; brand accents, system monospace and conversation surface colors. |
|
|
765
|
+
| `app-state.css` | Session page width, semantic background selection, observer connection/error presentation and native focus visibility. |
|
|
766
|
+
|
|
767
|
+
Session content, diagnostics and status inherit Pico's typography and colors.
|
|
768
|
+
Additional session CSS supplies geometry, overflow containment, declared glyphs,
|
|
769
|
+
visibility and the three backgrounds in {§web-conversation-surfaces}, not a
|
|
770
|
+
separate component theme. Layout uses
|
|
771
|
+
the substrate's spacing scale rather than a session-specific compact scale.
|
|
772
|
+
|
|
773
|
+
## §web-language Source language
|
|
774
|
+
|
|
775
|
+
The shipped document is English, declared by `html[lang]`. Its readable source
|
|
776
|
+
text remains in HTML; `data-i18n` keys identify text rather than appearing in its
|
|
777
|
+
place. OTM boot explicitly uses the document language and opts into no persistent
|
|
778
|
+
state. Browser language, query parameters and unrelated stored preferences do not
|
|
779
|
+
select an unshipped translation. No translation endpoint or duplicate source
|
|
780
|
+
dictionary is declared or packaged. Boot neither fetches a dictionary nor
|
|
781
|
+
rewrites the document into English, and completes without accessing browser
|
|
782
|
+
storage even when storage is denied. No language picker or additional locale is shipped.
|
|
783
|
+
|
|
784
|
+
## §web-authoring Browser boundaries
|
|
785
|
+
|
|
786
|
+
| Concern | Owner |
|
|
787
|
+
|---|---|
|
|
788
|
+
| Document structure and ordinary navigation | Semantic HTML, real links, lists and native forms; not a JavaScript-created application shell. |
|
|
789
|
+
| Interactive session | JavaScript-dependent product surface, not a parallel no-JS implementation. Status, conversation and explicit input use {§web-session-observer}, {§web-session-conversation} and {§web-session-input}. |
|
|
790
|
+
| Browser mechanisms | OTM public APIs and native controls; reusable gaps are repaired upstream rather than through local shims. |
|
|
791
|
+
| Document content | Native text or sanitized fragments enter template-owned outlets under {§web-conversation-content}. No competing state/translation writer or body attribute mirror. Unchanged text retains its nodes and native selection. |
|
|
792
|
+
| Visual state | Native element state or semantic attributes projected through CSS; no imperative style mutation. |
|
|
793
|
+
| Static checks | General JavaScript correctness and modern-idiom rules cover the server module, browser, tests and build tooling. OTM HTML/JavaScript/CSS rules and unsafe-HTML sink checks cover browser authoring. Separate server, browser and test type checks enforce public client types with DOM libraries and no Node globals in browser code; the application port remains server-only, and test callers are checked against module and browser signatures. |
|
|
794
|
+
|
|
795
|
+
## §web-browser-security Browser execution and diagnostics
|
|
796
|
+
|
|
797
|
+
| Boundary | Contract |
|
|
798
|
+
|---|---|
|
|
799
|
+
| Scripts and styles | Same-origin resources only; no inline execution or dynamic code generation. Browser schema validation runs without JIT, configured before SDK schema initialization. |
|
|
800
|
+
| Images | Same-origin and data-URI images, including Pico's embedded SVG controls. This permission does not extend to scripts, styles or connections. |
|
|
801
|
+
| Other directives | Default same-origin; no objects, base URL changes or framing. Forms submit to the same origin. |
|
|
802
|
+
| Browser gate | Capture console warnings/errors and uncaught exceptions from document, authentication, directory, SDK and actual session journeys. Healthy paths produce none. Negative cases assert their specific expected diagnostics; no blanket filtering of CSP or SDK errors. |
|
|
803
|
+
| Content checks | Adversarial compiled-renderer tests exercise HTML/SVG, executable URL schemes, application-binding injection, literal fields, streaming and replay in Chromium and Firefox. Assert preserved application structure, inert rejected links, no model-directed requests or new browsing contexts, and clean diagnostics. Math additionally exercises untrusted extensions, macro isolation and delayed rendering. A packed authenticated session receives hostile `note` and answer bodies through real SDK delivery and refresh. These checks verify concrete cases, not a claim of exhaustive security assurance. |
|
|
804
|
+
|
|
805
|
+
## §web-contract-checks Specification gate
|
|
806
|
+
|
|
807
|
+
The web lint gate uses the shared `@plurnk/plurnk-spec-check` development tool.
|
|
808
|
+
Named declarations are unique, citations resolve, and each contract has a
|
|
809
|
+
non-prose witness. No unwitnessed-contract allowance is used. The gate works from
|
|
810
|
+
a standalone checkout with installed dependencies, without a neighboring service
|
|
811
|
+
checkout. This checks traceability, not whether a cited test proves its contract.
|
|
812
|
+
|
|
813
|
+
## §web-build Development and production
|
|
814
|
+
|
|
815
|
+
| Command | Contract |
|
|
816
|
+
|---|---|
|
|
817
|
+
| `npm run dev` | Prepare static assets, then serve `web/` through Vite on loopback with HTML reload and CSS hot updates. No daemon or model is started. |
|
|
818
|
+
| `npm run build` | Prepare assets, clean and compile into `ai.plurnk/dist/`, then generate `ai.plurnk/assets.json` from the complete output. |
|
|
819
|
+
| `npm run preview` | Locally preview the last build through Vite; not a production server. |
|
|
820
|
+
| Daemon startup | Load the inventory and every listed production asset; serve them through the plugin's existing mount. No Vite runtime or development server. |
|
|
821
|
+
|
|
822
|
+
`web/index.html` owns the document, `web/app.js` imports OTM's npm entry,
|
|
823
|
+
and `web/public/` supplies unchanged public assets. The four CSS source
|
|
824
|
+
concerns remain under `web/styles/`. Generated output is packed, not committed.
|
|
825
|
+
Production contains no development-client injection, frontend sources, or source maps.
|
|
826
|
+
Current stable browsers are the supported platform. Compile native modern syntax;
|
|
827
|
+
do not add legacy browser transforms or a module-preload polyfill.
|
|
828
|
+
Development serves readable, complete Pico Classless Green. A CSS import entry
|
|
829
|
+
composes all four layers before production pruning. PostCSS/PurgeCSS scans the
|
|
830
|
+
browser source and server document renderer, preserves browser pseudo-states and
|
|
831
|
+
ARIA/role/data-driven states, and removes unused rules, variables and keyframes.
|
|
832
|
+
Vite then minifies the result. Browser coverage verifies the compiled presentation,
|
|
833
|
+
including error cards, native controls, light/dark mode and narrow viewports.
|
|
834
|
+
The current development page has no backend interaction; no API proxy is installed.
|
|
835
|
+
|
|
836
|
+
## §web-installation App identity and installation
|
|
837
|
+
|
|
838
|
+
| Surface | Contract |
|
|
839
|
+
|---|---|
|
|
840
|
+
| Manifest | Stable origin-relative `id`, `start_url` and `scope` of `/`; Plurnk name, English description, `standalone` display, no orientation lock. |
|
|
841
|
+
| Identity | Brand green `#148800`; Google Noto hamster SVG with glasses and a green bow, retaining Apache-2.0 licensing and modification attribution. |
|
|
842
|
+
| Icons | One SVG for the document, favicon and manifest; no raster fallback. A generated opaque, full-bleed maskable SVG places artwork inside the safe circle (40% of image width). Both manifest entries declare `sizes: any`. |
|
|
843
|
+
| Document metadata | UTF-8, language, zoomable viewport, description, application name, early `color-scheme: light dark` and brand theme color. The standard manifest owns standalone launch; no legacy platform-specific launch metadata. |
|
|
844
|
+
| Privacy | Discourage indexing with `noindex, nofollow`; limit referrers to the same origin. No external fonts, analytics or deployment-specific canonical URL. |
|
|
845
|
+
| Installation | Browser-owned installation and standalone launch. HTTPS is required remotely; localhost/loopback is eligible without TLS. Browser and OS support varies. |
|
|
846
|
+
| Availability | Requires the daemon. No offline cache, background sync, notifications or claim of offline functionality. |
|
|
847
|
+
|
|
848
|
+
Installation metadata does not create a second server or change ownership of
|
|
849
|
+
`/acp` or other daemon mounts. No in-page install prompt or application UI is added.
|
|
850
|
+
|
|
851
|
+
References: [installability](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable),
|
|
852
|
+
[manifest icons](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/icons).
|
|
853
|
+
|
|
854
|
+
## §web-package Package boundary
|
|
855
|
+
|
|
856
|
+
The npm archive includes the Agent Plugin manifest, native module, defaults,
|
|
857
|
+
document and its complete browser dependency graph, including OTM, Pico, the ACP SDK and artwork licenses,
|
|
858
|
+
and declares its peer ranges on the platform's contracts, modules, auth and ACP packages.
|
|
859
|
+
Tests and build tooling are not runtime inputs. Verification installs the archive
|
|
860
|
+
beside candidate service archives in an isolated environment and exercises actual module
|
|
861
|
+
discovery, routing and browser execution.
|
|
862
|
+
Shared-client review verification installs the terminal client archive beside the
|
|
863
|
+
same platform and web archives. A real PTY and headless terminal exercise both
|
|
864
|
+
manual/YOLO choices against the browser; tests observe one execution, convergent
|
|
865
|
+
results and withdrawn review controls, without another prompt or reconnection.
|
|
866
|
+
These drivers are test tooling, not runtime dependencies or a client transport.
|
|
867
|
+
|
|
868
|
+
Upstream contracts: [module lifecycle and mounts](https://github.com/plurnk/plurnk-service/blob/main/plurnk-modules/SPEC.md),
|
|
869
|
+
[Agent Plugins](https://agent-plugins.org/), and OTM's installed README.
|