@modelprofile.com/browser-runtime 3.1.1 → 4.0.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.
@@ -202,6 +202,7 @@ export interface IBrowserRuntimeState {
202
202
  lastError?: {
203
203
  code: string;
204
204
  fatal: boolean;
205
+ message?: string;
205
206
  tabId?: string;
206
207
  };
207
208
  }
@@ -289,6 +290,7 @@ export type TBrowserRuntimeEvent = {
289
290
  error: {
290
291
  code: string;
291
292
  fatal: boolean;
293
+ message?: string;
292
294
  tabId?: string;
293
295
  };
294
296
  } | {
@@ -1,5 +1,6 @@
1
1
  import type * as plugins from './plugins.js';
2
2
  import type { IBrowserConfinementProbeContext, IBrowserRuntimeFramedClientOptions, IBrowserRuntimeOptions, IBrowserRuntimeEnvironment, ILiveBrowserSessionLike } from './interfaces.js';
3
+ import type { IBrowserRuntimeOwnershipTestingOptions } from './classes.runtimeownership.js';
3
4
  export declare const browserRuntimeTesting: unique symbol;
4
5
  export interface IBrowserRuntimeTestingOptions extends IBrowserRuntimeOptions {
5
6
  [browserRuntimeTesting]: true;
@@ -11,6 +12,7 @@ export interface IBrowserRuntimeTestingOptions extends IBrowserRuntimeOptions {
11
12
  beforeFrameFailureTermination?(): Promise<void>;
12
13
  afterFrameFailureFence?(): Promise<void>;
13
14
  beforeFramedPeerLeasePublication?(): Promise<void>;
15
+ ownership?: IBrowserRuntimeOwnershipTestingOptions;
14
16
  }
15
17
  export interface IBrowserRuntimeFramedClientTestingOptions extends IBrowserRuntimeFramedClientOptions {
16
18
  [browserRuntimeTesting]: true;
@@ -1,2 +1,2 @@
1
1
  export const browserRuntimeTesting = Symbol('browserRuntimeTesting');
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJuYWwudGVzdGluZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2ludGVybmFsLnRlc3RpbmcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBU0EsTUFBTSxDQUFDLE1BQU0scUJBQXFCLEdBQUcsTUFBTSxDQUFDLHVCQUF1QixDQUFDLENBQUMifQ==
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJuYWwudGVzdGluZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2ludGVybmFsLnRlc3RpbmcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBVUEsTUFBTSxDQUFDLE1BQU0scUJBQXFCLEdBQUcsTUFBTSxDQUFDLHVCQUF1QixDQUFDLENBQUMifQ==
@@ -13,8 +13,9 @@ import * as url from 'node:url';
13
13
  export { Buffer, crypto, dns, fs, fsPromises, http, net, os, path, stream, tls, url };
14
14
  import * as flexharness from '@modelprofile.com/flexharness';
15
15
  import * as smartagent from '@push.rocks/smartagent';
16
+ import * as smartipc from '@push.rocks/smartipc';
16
17
  import * as smartmcp from '@push.rocks/smartmcp';
17
18
  import * as smartpuppeteer from '@push.rocks/smartpuppeteer';
18
- export { flexharness, smartagent, smartmcp, smartpuppeteer };
19
+ export { flexharness, smartagent, smartipc, smartmcp, smartpuppeteer };
19
20
  import ipaddr from 'ipaddr.js';
20
21
  export { ipaddr };
@@ -15,10 +15,11 @@ export { Buffer, crypto, dns, fs, fsPromises, http, net, os, path, stream, tls,
15
15
  // foss.global scopes
16
16
  import * as flexharness from '@modelprofile.com/flexharness';
17
17
  import * as smartagent from '@push.rocks/smartagent';
18
+ import * as smartipc from '@push.rocks/smartipc';
18
19
  import * as smartmcp from '@push.rocks/smartmcp';
19
20
  import * as smartpuppeteer from '@push.rocks/smartpuppeteer';
20
- export { flexharness, smartagent, smartmcp, smartpuppeteer };
21
+ export { flexharness, smartagent, smartipc, smartmcp, smartpuppeteer };
21
22
  // third-party scope
22
23
  import ipaddr from 'ipaddr.js';
23
24
  export { ipaddr };
24
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3BsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsb0JBQW9CO0FBQ3BCLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDckMsT0FBTyxLQUFLLE1BQU0sTUFBTSxhQUFhLENBQUM7QUFDdEMsT0FBTyxLQUFLLEdBQUcsTUFBTSxVQUFVLENBQUM7QUFDaEMsT0FBTyxLQUFLLEVBQUUsTUFBTSxTQUFTLENBQUM7QUFDOUIsT0FBTyxLQUFLLFVBQVUsTUFBTSxrQkFBa0IsQ0FBQztBQUMvQyxPQUFPLEtBQUssSUFBSSxNQUFNLFdBQVcsQ0FBQztBQUNsQyxPQUFPLEtBQUssR0FBRyxNQUFNLFVBQVUsQ0FBQztBQUNoQyxPQUFPLEtBQUssRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUM5QixPQUFPLEtBQUssSUFBSSxNQUFNLFdBQVcsQ0FBQztBQUNsQyxPQUFPLEtBQUssTUFBTSxNQUFNLGFBQWEsQ0FBQztBQUN0QyxPQUFPLEtBQUssR0FBRyxNQUFNLFVBQVUsQ0FBQztBQUNoQyxPQUFPLEtBQUssR0FBRyxNQUFNLFVBQVUsQ0FBQztBQUVoQyxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxHQUFHLEVBQUUsRUFBRSxFQUFFLFVBQVUsRUFBRSxJQUFJLEVBQUUsR0FBRyxFQUFFLEVBQUUsRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLEdBQUcsRUFBRSxHQUFHLEVBQUUsQ0FBQztBQUV0RixxQkFBcUI7QUFDckIsT0FBTyxLQUFLLFdBQVcsTUFBTSwrQkFBK0IsQ0FBQztBQUM3RCxPQUFPLEtBQUssVUFBVSxNQUFNLHdCQUF3QixDQUFDO0FBQ3JELE9BQU8sS0FBSyxRQUFRLE1BQU0sc0JBQXNCLENBQUM7QUFDakQsT0FBTyxLQUFLLGNBQWMsTUFBTSw0QkFBNEIsQ0FBQztBQUU3RCxPQUFPLEVBQUUsV0FBVyxFQUFFLFVBQVUsRUFBRSxRQUFRLEVBQUUsY0FBYyxFQUFFLENBQUM7QUFFN0Qsb0JBQW9CO0FBQ3BCLE9BQU8sTUFBTSxNQUFNLFdBQVcsQ0FBQztBQUUvQixPQUFPLEVBQUUsTUFBTSxFQUFFLENBQUMifQ==
25
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3BsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsb0JBQW9CO0FBQ3BCLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDckMsT0FBTyxLQUFLLE1BQU0sTUFBTSxhQUFhLENBQUM7QUFDdEMsT0FBTyxLQUFLLEdBQUcsTUFBTSxVQUFVLENBQUM7QUFDaEMsT0FBTyxLQUFLLEVBQUUsTUFBTSxTQUFTLENBQUM7QUFDOUIsT0FBTyxLQUFLLFVBQVUsTUFBTSxrQkFBa0IsQ0FBQztBQUMvQyxPQUFPLEtBQUssSUFBSSxNQUFNLFdBQVcsQ0FBQztBQUNsQyxPQUFPLEtBQUssR0FBRyxNQUFNLFVBQVUsQ0FBQztBQUNoQyxPQUFPLEtBQUssRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUM5QixPQUFPLEtBQUssSUFBSSxNQUFNLFdBQVcsQ0FBQztBQUNsQyxPQUFPLEtBQUssTUFBTSxNQUFNLGFBQWEsQ0FBQztBQUN0QyxPQUFPLEtBQUssR0FBRyxNQUFNLFVBQVUsQ0FBQztBQUNoQyxPQUFPLEtBQUssR0FBRyxNQUFNLFVBQVUsQ0FBQztBQUVoQyxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxHQUFHLEVBQUUsRUFBRSxFQUFFLFVBQVUsRUFBRSxJQUFJLEVBQUUsR0FBRyxFQUFFLEVBQUUsRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLEdBQUcsRUFBRSxHQUFHLEVBQUUsQ0FBQztBQUV0RixxQkFBcUI7QUFDckIsT0FBTyxLQUFLLFdBQVcsTUFBTSwrQkFBK0IsQ0FBQztBQUM3RCxPQUFPLEtBQUssVUFBVSxNQUFNLHdCQUF3QixDQUFDO0FBQ3JELE9BQU8sS0FBSyxRQUFRLE1BQU0sc0JBQXNCLENBQUM7QUFDakQsT0FBTyxLQUFLLFFBQVEsTUFBTSxzQkFBc0IsQ0FBQztBQUNqRCxPQUFPLEtBQUssY0FBYyxNQUFNLDRCQUE0QixDQUFDO0FBRTdELE9BQU8sRUFBRSxXQUFXLEVBQUUsVUFBVSxFQUFFLFFBQVEsRUFBRSxRQUFRLEVBQUUsY0FBYyxFQUFFLENBQUM7QUFFdkUsb0JBQW9CO0FBQ3BCLE9BQU8sTUFBTSxNQUFNLFdBQVcsQ0FBQztBQUUvQixPQUFPLEVBQUUsTUFBTSxFQUFFLENBQUMifQ==
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelprofile.com/browser-runtime",
3
- "version": "3.1.1",
3
+ "version": "4.0.0",
4
4
  "private": false,
5
5
  "description": "Parent-owned, resource-centric Chromium runtime with revisioned attachment fencing, authenticated human and agent control, fail-closed egress, bounded artifacts, and Flex/MCP adapters.",
6
6
  "main": "dist_ts/index.js",
@@ -23,13 +23,14 @@
23
23
  "url": "https://community.foss.global/"
24
24
  },
25
25
  "engines": {
26
- "node": ">=24"
26
+ "node": ">=24 <26"
27
27
  },
28
28
  "dependencies": {
29
29
  "@modelprofile.com/flexharness": "^4.0.0",
30
30
  "@push.rocks/smartagent": "^4.8.0",
31
+ "@push.rocks/smartipc": "^2.4.2",
31
32
  "@push.rocks/smartmcp": "^0.3.0",
32
- "@push.rocks/smartpuppeteer": "^2.5.0",
33
+ "@push.rocks/smartpuppeteer": "^2.6.0",
33
34
  "ipaddr.js": "^2.5.0"
34
35
  },
35
36
  "devDependencies": {
package/readme.hints.md CHANGED
@@ -4,6 +4,13 @@ Durable implementation findings for `@modelprofile.com/browser-runtime`.
4
4
 
5
5
  ## Ownership and lifecycle
6
6
 
7
+ - Host-runtime liveness is owned by SmartIPC `NamedMutex` using a namespace bound to the UID and secured runtime-directory device, inode, and path hash. Its permanent private `runtime.mutex` anchor directory and file stay on the runtime directory's trusted local filesystem. Clean native contention is `LOCKED`; backend, anchor, lock, or release uncertainty is `FENCED`.
8
+ - Persistent, fsynced `runtime.lock` metadata is a downgrade fence, not the live mutex, and is never removed by normal shutdown. Every start owns one metadata-named generation containing private profile and artifact roots.
9
+ - Metadata-generation recovery performs no lock-descriptor scan. It removes only the exact named generation after checking every same-UID process for an absolute `--user-data-dir` equal to or below that profile root; this check is process-generic, not Chromium-specific.
10
+ - Process ownership comes from all four `/proc/<pid>/status` `Uid:` values, not `/proc/<pid>` directory ownership. Only an exact same-UID or definite other-UID classification is actionable; mixed, unreadable, malformed, symlinked, wrongly owned/mode, hard-linked, ambiguous, or uncontained ownership state remains fenced.
11
+ - Legacy zero-byte locks alone use exact-inode descriptor inspection. Adoption additionally requires birth, change, and modification timestamps strictly before bounded `/proc/stat` `btime`, plus no same-UID process using the exact or descendant legacy profile root. Same-boot, malformed, or ambiguous state remains fenced against the 3.2 close-before-unlink race.
12
+ - A clean 3.2 stopped layout has no lock and may retain private empty top-level profile and artifact roots. The native-mutex winner validates, process-checks, and removes only those roots. Before metadata fsync succeeds, failure cleanup may unlink only the exact lock inode that acquisition just created; preexisting locks are never removed.
13
+ - Shutdown attempts artifact and ownership cleanup after peer, revocation, or resource failures, retains retryable local slot state, and blocks restart until that state clears. Automatic ownership-loss cleanup gets one bounded retry; stale active metadata remains a fail-closed recovery fence.
7
14
  - Controller durable truth is a logical resource with immutable `projectId`, stable random `browserResourceId`, and a revisioned attachment binding. Runtime state is process-local and must be re-registered after every restart.
8
15
  - A resource slot owns only incarnation and arbitration state: its transition mutex, arbitration, incarnation, and process-local authority generations, lease, bounded operation FIFO, bounded frame window, Chromium session, authenticated egress proxy, private profile, and idle termination timer.
9
16
  - `terminateResource()` and idle termination remove only the incarnation. They preserve registration, attachment, capabilities, and artifacts. `retireResource()` permanently fences, revokes, terminates, purges exact-resource artifacts, then unregisters only after all cleanup succeeds. Durable retirement remains Controller truth; Runtime tombstones are bounded and process-local.
@@ -42,5 +49,6 @@ Durable implementation findings for `@modelprofile.com/browser-runtime`.
42
49
  - Framed request counts and queued write bytes are bounded. Runtime shutdown has a bounded caller-visible cleanup deadline while retaining in-flight cleanup ownership for retry.
43
50
  - Frame delivery owns a bounded map of exact identities and configures SmartPuppeteer's separate private CDP-to-ack map to the same limit. Equal limits prevent private eviction while the application map has spare capacity, but private-first retirement at full capacity can make an exact acknowledgement fulfill `{ accepted: false }`. Both fulfilled booleans settle the application entry without proving successful upstream retirement; rejection or timeout fails closed. Application acknowledgement timeout and failed delivery deliberately fail the lease regardless of a fulfilled boolean. There is no cumulative frame protocol.
44
51
  - During refresh, application entries plus refresh-owned acknowledgement promises share the same hard frame-window bound. ACK ownership is fixed at admission, refresh failures defer until the operation reservation is gone, and the exact failure fence completes before terminal audit, caller settlement, or FIFO advancement. Lifecycle teardown explicitly abandons an exact refresh record so stale state cannot retain `BUSY` admission.
52
+ - Runtime state and error events preserve diagnostic messages only after truncating them to 2,048 characters. The first fatal session error invalidates exact capability admission synchronously, delivers once to the captured exact subscriber, and schedules revocation in `finally` so a throwing listener cannot retain authority.
45
53
  - Flex resolves only a capability token, and the provider's run must exactly match its trusted framed client. SmartAgent exposes exactly navigate, snapshot, screenshot, click, fill, and press.
46
54
  - MCP independent authentication returns the complete expected binding. The binding is retained in server-owned auth context and tool schemas expose no resource, session, revision, or authority selector.
package/readme.md CHANGED
@@ -12,11 +12,12 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
12
12
  pnpm add @modelprofile.com/browser-runtime
13
13
  ```
14
14
 
15
- The runtime requires Node.js 24 or newer and a non-root Linux host. Production browser sessions require a sandbox-capable Chromium installation.
15
+ The runtime requires Node.js 24 or 25 on a non-root Linux x64 host. Production browser sessions require a sandbox-capable Chromium installation.
16
+ The `NamedMutex` backend requires the `fs-ext-extra-prebuilt@2.2.9` install script to be approved by pnpm; startup fails closed with `FENCED` when the native backend is unavailable.
16
17
 
17
18
  ## Resource Model
18
19
 
19
- The Controller owns durable resource and attachment truth. `BrowserRuntime` owns process-local registrations, fences, capabilities, leases, browser incarnations, artifacts, and its runtime lock. Registration never launches Chromium.
20
+ The Controller owns durable resource and attachment truth. `BrowserRuntime` owns process-local registrations, fences, capabilities, leases, browser incarnations, artifacts, and its host-runtime ownership lease. Registration never launches Chromium.
20
21
 
21
22
  ```typescript
22
23
  import {
@@ -73,7 +74,25 @@ try {
73
74
 
74
75
  Projects and qualified sessions may each own many resources. Human/agent arbitration, mutexes, leases, operation queues, profiles, proxies, frame subscriptions, and idle timers are per resource.
75
76
 
76
- `registerResource()` is idempotent only for the same project/resource key and identical attachment. `listResources()` reports process-local registration and incarnation metadata. `terminateResource()` terminates only the current incarnation and preserves registration, attachment, and artifacts. `retireResource()` permanently fences the process-local registration, revokes and quiesces authority, terminates its incarnation, purges exact-resource artifacts, and unregisters only after cleanup succeeds. The Controller separately owns durable retirement truth and must not rehydrate retired resources. Runtime tombstones and registrations are ephemeral, bounded process state. `stop()` revokes all process-local capabilities, terminates every incarnation, removes the Runtime-owned artifact root and registrations, and releases the lock without deleting Controller durable truth. `cleanupTimeoutMs` defaults to 30,000 milliseconds and accepts values from 100 through 120,000. If that caller-visible deadline expires, `stop()` rejects with `TIMEOUT` while Runtime retains ownership of the cleanup still in flight; a later `stop()` rejoins it, and `start()` waits for it to finish before restarting.
77
+ `registerResource()` is idempotent only for the same project/resource key and identical attachment. `listResources()` reports process-local registration and incarnation metadata. `terminateResource()` terminates only the current incarnation and preserves registration, attachment, and artifacts. `retireResource()` permanently fences the process-local registration, revokes and quiesces authority, terminates its incarnation, purges exact-resource artifacts, and unregisters only after cleanup succeeds. The Controller separately owns durable retirement truth and must not rehydrate retired resources. Runtime tombstones and registrations are ephemeral, bounded process state. `stop()` attempts peer, capability, resource, artifact-store, and ownership cleanup even when an earlier stage fails, then reports all failures together. It removes only the current generation, durably marks its owner metadata relinquished, and releases the native ownership lease when ownership cleanup succeeds. It intentionally leaves `runtime.lock` and the mutex anchor in place. Failed local slot cleanup remains retryable and blocks a new generation; automatic ownership-loss shutdown performs one additional bounded cleanup attempt. `cleanupTimeoutMs` defaults to 30,000 milliseconds and accepts values from 100 through 120,000. If that caller-visible deadline expires, `stop()` rejects with `TIMEOUT` while Runtime retains cleanup still in flight; a later `stop()` rejoins it, and `start()` waits for all prior local cleanup before restarting.
78
+
79
+ ## Host Runtime Ownership
80
+
81
+ The runtime directory must be an owner-only `0700` directory on a trusted local filesystem; NFS and other network filesystems are unsupported. Its permanent `runtime.mutex` anchor directory is an owner-only `0700` directory on the same filesystem device. Runtime uses `@push.rocks/smartipc` `NamedMutex` with a namespace bound to the UID plus the validated runtime-directory device, inode, and path hash. SmartIPC keeps one permanent `0600`, single-link anchor file in `runtime.mutex` and holds a native advisory lock for the complete metadata and generation operation. Clean `tryAcquire()` contention returns `LOCKED`. Unsupported native backends, unsafe or changed anchors, native lock failures, and uncertain release return `FENCED`. Process exit, `SIGKILL`, and reboot release the advisory lock in the kernel.
82
+
83
+ `runtime.lock` is a permanent `0600`, single-link regular file retained through an open descriptor while ownership is live. It is metadata and a downgrade fence, not the live mutex, and is never removed by `stop()`. Version 3.2 and older create-exclusive runtimes therefore remain fenced during and between new-runtime generations. Bounded JSON metadata is overwritten and fsynced through that descriptor. It records the schema, directory identity, UID, boot ID, PID, `/proc/self/stat` start ticks, generation ID, nonce, and active/relinquished state. Malformed metadata or unsafe directory, anchor, lock, link, ownership, mode, containment, or inspection state fails closed with `FENCED`.
84
+
85
+ Each successful `start()` creates private `generations/<generationId>/profiles` and `generations/<generationId>/artifacts` directories. A replacement holding the native mutex may inspect and remove only the generation named by valid metadata. For metadata generations, Runtime does not scan `runtime.lock` descriptors. It inspects every same-UID process and returns `LOCKED` when any process, Chromium or otherwise, has an absolute `--user-data-dir` equal to or below that generation's profile root. Process ownership is classified from all four IDs in `/proc/<pid>/status` `Uid:`: exact same-UID processes are inspected, definite other-UID processes are ignored, and mixed or unreadable identity remains indeterminate. Unknown generations and indeterminate inspection remain fenced.
86
+
87
+ A version 3.2 zero-byte lock uses a separate one-time migration gate. Runtime adopts it only when an exact-inode legacy descriptor scan finds no holder, the lock's birth, change, and modification timestamps are all strictly before the bounded `/proc/stat` `btime`, and no same-UID process has `--user-data-dir` equal to or below the legacy `profiles` root. A same-boot unheld lock is `FENCED`; this prevents takeover during the old owner's close-before-unlink window. Malformed boot data or ambiguous timestamps are also `FENCED`.
88
+
89
+ When version 3.2 stopped cleanly and removed `runtime.lock`, it may leave empty top-level `profiles` and `artifacts` directories. After winning creation of a new lock under the native mutex, Runtime validates those directories as exact private empty roots, performs the same exact-or-descendant profile-process inspection, removes only the validated roots, and publishes the new metadata without applying the stale-lock boot gate. If startup fails before metadata is durably published, Runtime unlinks only that exact process-created lock inode while still holding the native mutex; a preexisting lock is never removed.
90
+
91
+ ## Breaking Changes
92
+
93
+ The version 3.2 runtime-directory migration is one-way. A cleanly stopped 3.2 runtime that removed its zero-byte lock can be upgraded directly when any remaining top-level `profiles` and `artifacts` directories are private and empty. A stale 3.2 lock is recoverable only after reboot, once it conclusively predates the current boot and no legacy holder or profile process remains. The first successful new-runtime start writes permanent metadata and creates `runtime.mutex`; version 3.2 must not reuse that directory.
94
+
95
+ To downgrade, first stop the new runtime and verify that no process still uses any generation profile path. Then remove the entire runtime directory, including `runtime.lock`, `runtime.mutex`, and `generations`, and let version 3.2 create a fresh directory. Never delete only the lock or anchor, and never perform this procedure while either runtime generation is active.
77
96
 
78
97
  ## Attachment Fencing
79
98
 
@@ -105,6 +124,8 @@ Operations from one valid lease enter a strict bounded FIFO. `maxQueuedOperation
105
124
 
106
125
  Human frame delivery keeps a bounded exact-identity application window. `maxOutstandingFrames` defaults to 4 and accepts values from 1 through 32. Runtime passes the same bound to SmartPuppeteer's private CDP-to-ack window; the windows remain separately owned, but equal limits prevent private eviction while the application window still has spare capacity. At full capacity SmartPuppeteer retires its oldest private entry before publishing the next frame, so a later exact acknowledgement can fulfill with `{ accepted: false }`. A fulfilled `true` or `false` settles the Runtime entry locally without fencing; `false` does not prove that upstream retirement succeeded. Rejected or timed-out acknowledgement operations fail closed, as do application acknowledgement timeout and failed-delivery retirement regardless of a fulfilled boolean. Frame acknowledgement stays outside the operation FIFO.
107
126
 
127
+ Human state and error events retain an optional diagnostic `message` bounded to 2,048 characters. A fatal session error synchronously invalidates the exact capability before its captured subscriber is notified once, then revokes and cleans up the lease even when that subscriber throws.
128
+
108
129
  `humanLease.refreshFrameStream(options?: IBrowserRuntimeOperationOptions): Promise<void>` requires the lease's exact active frame subscription and enters the same audited FIFO as other browser mutations. Runtime retires the old application window, asks SmartPuppeteer for a new screencast generation, and succeeds only after the exact first new-generation frame was delivered through that subscription and validated against the current tab, viewport, lease, session, and incarnation. Existing and refresh-owned acknowledgement work share `maxOutstandingFrames`; synchronous listener acknowledgements cannot create an unbounded promise set. A missing or pre-execution-replaced subscription fails with `BUSY`. Refresh protocol, delivery, retirement, or acknowledgement failure returns `FRAME_STREAM_FAILED` or `FRAME_TOO_LARGE` only after the exact incarnation is fenced; stale authority returns `ABORTED` without touching its replacement. Pass `options.signal` for caller cancellation; active cancellation preserves a successfully restored stream.
109
130
 
110
131
  ## Trusted Pipe And Flex
@@ -136,7 +157,7 @@ pnpm test
136
157
  pnpm run test:real-chrome
137
158
  ```
138
159
 
139
- The real Chromium check launches two resources in one project and qualified session, verifies distinct private profiles, sandboxed renderer process trees, mandatory confinement, screenshots, confirmed shutdown, and profile deletion.
160
+ The real Chromium check launches two resources in one project and qualified session, verifies distinct generation-scoped private profiles, sandboxed renderer process trees, mandatory confinement, screenshots, confirmed shutdown, and generation deletion.
140
161
 
141
162
  ## License and Legal Information
142
163
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/browser-runtime',
6
- version: '3.1.1',
6
+ version: '4.0.0',
7
7
  description: 'Parent-owned, resource-centric Chromium runtime with revisioned attachment fencing, authenticated human and agent control, fail-closed egress, bounded artifacts, and Flex/MCP adapters.'
8
8
  }
package/ts/actions.ts CHANGED
@@ -184,9 +184,17 @@ const validateRuntimeState = (value: unknown): IBrowserRuntimeState => {
184
184
  );
185
185
  }
186
186
  if (state.lastError !== undefined) {
187
- const lastError = validateExactKeys(state.lastError, ['code', 'fatal', 'tabId'], 'lastError');
187
+ const lastError = validateExactKeys(
188
+ state.lastError,
189
+ ['code', 'fatal', 'message', 'tabId'],
190
+ 'lastError',
191
+ );
188
192
  validateBoundedString(lastError.code, 'lastError.code', 1, 128);
189
193
  if (typeof lastError.fatal !== 'boolean') throw new BrowserRuntimeError('PROTOCOL_ERROR');
194
+ if (
195
+ lastError.message !== undefined
196
+ && (typeof lastError.message !== 'string' || lastError.message.length > 2048)
197
+ ) throw new BrowserRuntimeError('PROTOCOL_ERROR');
190
198
  if (lastError.tabId !== undefined) validateBoundedString(lastError.tabId, 'lastError.tabId', 1, 128);
191
199
  }
192
200
  return value as IBrowserRuntimeState;
@@ -32,7 +32,7 @@ interface IProjectArtifacts {
32
32
  }
33
33
 
34
34
  export class BrowserArtifactStore {
35
- private readonly rootDirectory: string;
35
+ private rootDirectory: string;
36
36
  private readonly maxFileBytes: number;
37
37
  private readonly maxArtifactsPerResource: number;
38
38
  private readonly maxResourceBytes: number;
@@ -127,6 +127,17 @@ export class BrowserArtifactStore {
127
127
  return this.startPromise;
128
128
  }
129
129
 
130
+ /** @internal */
131
+ public setRootDirectoryForRuntime(rootDirectoryArg: string): void {
132
+ if (
133
+ !plugins.path.isAbsolute(rootDirectoryArg)
134
+ || this.started
135
+ || this.startPromise
136
+ || this.stopPromise
137
+ ) throw new BrowserRuntimeError('FENCED', 'artifact store cannot change generations');
138
+ this.rootDirectory = rootDirectoryArg;
139
+ }
140
+
130
141
  public stop(): Promise<void> {
131
142
  if (this.stopPromise) return this.stopPromise;
132
143
  if (!this.started && !this.startPromise) return Promise.resolve();
@@ -384,15 +395,7 @@ export class BrowserArtifactStore {
384
395
  }
385
396
  throw error;
386
397
  }
387
- const stat = await plugins.fsPromises.lstat(this.rootDirectory);
388
- const uid = process.getuid?.();
389
- if (
390
- !stat.isDirectory()
391
- || stat.isSymbolicLink()
392
- || (uid !== undefined && stat.uid !== uid)
393
- || (stat.mode & 0o777) !== 0o700
394
- ) throw new BrowserRuntimeError('FENCED', 'artifact root is not private');
395
- await plugins.fsPromises.chmod(this.rootDirectory, 0o700);
398
+ await this.secureNewDirectory(this.rootDirectory);
396
399
  this.started = true;
397
400
  const sweepIntervalMs = Math.min(60_000, Math.max(1000, Math.floor(this.artifactTtlMs / 2)));
398
401
  this.sweepTimer = setInterval(() => {
@@ -411,10 +414,12 @@ export class BrowserArtifactStore {
411
414
 
412
415
  private async createProject(projectId: string): Promise<IProjectArtifacts> {
413
416
  const directoryName = this.digestDirectory(`project\0${projectId}`);
414
- await plugins.fsPromises.mkdir(plugins.path.join(this.rootDirectory, directoryName), {
417
+ const projectDirectory = plugins.path.join(this.rootDirectory, directoryName);
418
+ await plugins.fsPromises.mkdir(projectDirectory, {
415
419
  recursive: false,
416
420
  mode: 0o700,
417
421
  });
422
+ await this.secureNewDirectory(projectDirectory);
418
423
  const project: IProjectArtifacts = {
419
424
  directoryName,
420
425
  totalBytes: 0,
@@ -431,10 +436,13 @@ export class BrowserArtifactStore {
431
436
  browserResourceId: string,
432
437
  ): Promise<IResourceArtifacts> {
433
438
  const directoryName = this.digestDirectory(`resource\0${projectId}\0${browserResourceId}`);
434
- await plugins.fsPromises.mkdir(
435
- plugins.path.join(this.rootDirectory, project.directoryName, directoryName),
436
- { recursive: false, mode: 0o700 },
439
+ const resourceDirectory = plugins.path.join(
440
+ this.rootDirectory,
441
+ project.directoryName,
442
+ directoryName,
437
443
  );
444
+ await plugins.fsPromises.mkdir(resourceDirectory, { recursive: false, mode: 0o700 });
445
+ await this.secureNewDirectory(resourceDirectory);
438
446
  const resource: IResourceArtifacts = {
439
447
  browserResourceId,
440
448
  directoryName,
@@ -445,6 +453,35 @@ export class BrowserArtifactStore {
445
453
  return resource;
446
454
  }
447
455
 
456
+ private async secureNewDirectory(pathArg: string): Promise<void> {
457
+ await plugins.fsPromises.chmod(pathArg, 0o700);
458
+ const handle = await plugins.fsPromises.open(
459
+ pathArg,
460
+ plugins.fs.constants.O_RDONLY
461
+ | plugins.fs.constants.O_DIRECTORY
462
+ | plugins.fs.constants.O_NOFOLLOW,
463
+ );
464
+ try {
465
+ await handle.chmod(0o700);
466
+ await plugins.fsPromises.chmod(pathArg, 0o700);
467
+ const [pathStat, handleStat] = await Promise.all([
468
+ plugins.fsPromises.lstat(pathArg, { bigint: true }),
469
+ handle.stat({ bigint: true }),
470
+ ]);
471
+ const uid = process.getuid?.();
472
+ if (
473
+ !pathStat.isDirectory()
474
+ || pathStat.isSymbolicLink()
475
+ || (uid !== undefined && pathStat.uid !== BigInt(uid))
476
+ || (pathStat.mode & 0o777n) !== 0o700n
477
+ || pathStat.dev !== handleStat.dev
478
+ || pathStat.ino !== handleStat.ino
479
+ ) throw new BrowserRuntimeError('FENCED', 'artifact directory is not private');
480
+ } finally {
481
+ await handle.close();
482
+ }
483
+ }
484
+
448
485
  private async purgeExpiredInternal(resourceKey?: IBrowserResourceKey): Promise<number> {
449
486
  const projectEntries = resourceKey
450
487
  ? [[resourceKey.projectId, this.projects.get(resourceKey.projectId)] as const]