@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.
- package/changelog.md +27 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/actions.js +5 -2
- package/dist_ts/classes.artifactstore.d.ts +4 -1
- package/dist_ts/classes.artifactstore.js +42 -11
- package/dist_ts/classes.runtime.d.ts +8 -5
- package/dist_ts/classes.runtime.js +172 -64
- package/dist_ts/classes.runtimeownership.d.ts +102 -0
- package/dist_ts/classes.runtimeownership.js +1230 -0
- package/dist_ts/interfaces.d.ts +2 -0
- package/dist_ts/internal.testing.d.ts +2 -0
- package/dist_ts/internal.testing.js +1 -1
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/package.json +4 -3
- package/readme.hints.md +8 -0
- package/readme.md +25 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/actions.ts +9 -1
- package/ts/classes.artifactstore.ts +51 -14
- package/ts/classes.runtime.ts +172 -61
- package/ts/classes.runtimeownership.ts +1393 -0
- package/ts/interfaces.ts +5 -1
- package/ts/internal.testing.ts +2 -0
- package/ts/plugins.ts +2 -1
package/dist_ts/interfaces.d.ts
CHANGED
|
@@ -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,
|
|
2
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJuYWwudGVzdGluZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2ludGVybmFsLnRlc3RpbmcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBVUEsTUFBTSxDQUFDLE1BQU0scUJBQXFCLEdBQUcsTUFBTSxDQUFDLHVCQUF1QixDQUFDLENBQUMifQ==
|
package/dist_ts/plugins.d.ts
CHANGED
|
@@ -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 };
|
package/dist_ts/plugins.js
CHANGED
|
@@ -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,
|
|
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
|
+
"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.
|
|
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
|
|
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
|
|
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()`
|
|
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
|
|
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
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@modelprofile.com/browser-runtime',
|
|
6
|
-
version: '
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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]
|