@modelprofile.com/browser-runtime 5.7.1 → 5.8.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 +24 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/actions.js +36 -2
- package/dist_ts/classes.flexprovider.js +2 -2
- package/dist_ts/classes.framed.d.ts +1 -1
- package/dist_ts/classes.runtimeownership.d.ts +11 -2
- package/dist_ts/classes.runtimeownership.js +126 -48
- package/dist_ts/plugins.d.ts +2 -2
- package/dist_ts/plugins.js +3 -3
- package/package.json +2 -4
- package/readme.md +6 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/actions.ts +35 -1
- package/ts/classes.flexprovider.ts +2 -2
- package/ts/classes.framed.ts +1 -1
- package/ts/classes.runtimeownership.ts +138 -47
- package/ts/plugins.ts +2 -2
- package/.smartconfig.json +0 -34
package/readme.md
CHANGED
|
@@ -129,9 +129,13 @@ The runtime directory must be an owner-only `0700` directory on a trusted local
|
|
|
129
129
|
|
|
130
130
|
`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`.
|
|
131
131
|
|
|
132
|
-
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
|
|
132
|
+
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 candidate 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. Unknown generations and indeterminate inspection remain fenced.
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
The inspection exists only to protect a profile root that is about to be removed, so when no inspected profile root is present the process table is never enumerated; an unreadable profile root is `FENCED`. Process identity is classified from all four IDs in `/proc/<pid>/status` `Uid:`. Same-UID processes and shared-UID processes — an ordinary setuid credential transition such as `sudo`, `su` or `passwd`, where this UID appears in some but not all four IDs — are both candidates; definite other-UID processes are ignored. A shared UID is never a fence by itself, and a single such process anywhere in `/proc` must never fence startup: the candidate's command line decides it. A readable command line naming no `--user-data-dir` at or below an inspected root proves the process is not using it. A candidate whose command line cannot be read is fail-closed and remains indeterminate, as does unparseable `Uid:` state.
|
|
135
|
+
|
|
136
|
+
An empty command line is not a decision either. A process caught mid-`execve` or past `exit_mm` publishes one for a bounded window while `State:` still reports it running, so Runtime re-reads the identity and the command line over that window — five attempts 50 ms apart, at most 200 ms of waiting per candidate — before deciding. A departed PID, a definite other UID, and a zombie of any candidate UID state are clear; a command line that appears within the window takes the ordinary containment path and can still return `LOCKED`; a candidate still running and still command-less after the window is `FENCED`.
|
|
137
|
+
|
|
138
|
+
A version 3.2 zero-byte lock uses a separate one-time migration gate. Runtime adopts it only when SmartIPC's consuming kernel exclusivity probe reports no other read/write open description, the lock's birth, change, and modification timestamps are all strictly before the bounded `/proc/stat` `btime`, and, when that root is present, no candidate process has `--user-data-dir` equal to or below the legacy `profiles` root. The exact original inode, ownership, mode, link count, zero size, and timestamps are revalidated when the lock is reopened after probing. Probe contention is `LOCKED`; probe or reopen uncertainty is `FENCED`. `O_PATH` descriptors do not contend. 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`.
|
|
135
139
|
|
|
136
140
|
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.
|
|
137
141
|
|
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: '5.
|
|
6
|
+
version: '5.8.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
|
@@ -130,7 +130,7 @@ export const validateAgentAction = (value: unknown): TBrowserAgentAction => {
|
|
|
130
130
|
const validateRuntimeState = (value: unknown): IBrowserRuntimeState => {
|
|
131
131
|
const state = validateExactKeys(
|
|
132
132
|
value,
|
|
133
|
-
['status', 'activeTabId', 'viewportRevision', 'viewport', 'tabs', 'lastError', 'videoAcceleration'],
|
|
133
|
+
['status', 'activeTabId', 'viewportRevision', 'viewport', 'tabs', 'lastError', 'videoAcceleration', 'videoSource'],
|
|
134
134
|
'browser state',
|
|
135
135
|
);
|
|
136
136
|
if (
|
|
@@ -167,6 +167,7 @@ const validateRuntimeState = (value: unknown): IBrowserRuntimeState => {
|
|
|
167
167
|
'generation',
|
|
168
168
|
'appliedViewportRevision',
|
|
169
169
|
'streaming',
|
|
170
|
+
'dialog',
|
|
170
171
|
], 'tab');
|
|
171
172
|
validateBoundedString(tab.id, 'tab.id', 1, 128);
|
|
172
173
|
if (typeof tab.url !== 'string' || tab.url.length > 8192) throw new BrowserRuntimeError('PROTOCOL_ERROR');
|
|
@@ -182,6 +183,39 @@ const validateRuntimeState = (value: unknown): IBrowserRuntimeState => {
|
|
|
182
183
|
0,
|
|
183
184
|
Number.MAX_SAFE_INTEGER,
|
|
184
185
|
);
|
|
186
|
+
if (tab.dialog !== undefined) {
|
|
187
|
+
const dialog = validateExactKeys(tab.dialog, ['id', 'type', 'message', 'defaultPrompt', 'url'], 'dialog');
|
|
188
|
+
validateBoundedString(dialog.id, 'dialog.id', 1, 128);
|
|
189
|
+
if (!['alert', 'confirm', 'prompt', 'beforeunload'].includes(dialog.type as string)) {
|
|
190
|
+
throw new BrowserRuntimeError('PROTOCOL_ERROR');
|
|
191
|
+
}
|
|
192
|
+
for (const [key, maximum] of [['message', 16_384], ['defaultPrompt', 4096], ['url', 4096]] as const) {
|
|
193
|
+
if (typeof dialog[key] !== 'string' || dialog[key].length > maximum) {
|
|
194
|
+
throw new BrowserRuntimeError('PROTOCOL_ERROR');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (state.videoSource !== undefined) {
|
|
200
|
+
const source = validateExactKeys(state.videoSource,
|
|
201
|
+
['tabId', 'generation', 'viewportRevision', 'viewport', 'frameAlignment', 'rtpTimestampFloor'], 'videoSource');
|
|
202
|
+
validateBoundedString(source.tabId, 'videoSource.tabId', 1, 128);
|
|
203
|
+
validateInteger(source.generation, 'videoSource.generation', 0, Number.MAX_SAFE_INTEGER);
|
|
204
|
+
validateInteger(source.viewportRevision, 'videoSource.viewportRevision', 0, Number.MAX_SAFE_INTEGER);
|
|
205
|
+
const sourceViewport = validateExactKeys(source.viewport, ['width', 'height', 'deviceScaleFactor'], 'videoSource.viewport');
|
|
206
|
+
const matchingTabs = (state.tabs as IBrowserRuntimeState['tabs']).filter(tab => tab.id === source.tabId);
|
|
207
|
+
const tab = matchingTabs[0];
|
|
208
|
+
if (matchingTabs.length !== 1 || !tab?.active || !tab.streaming || tab.status !== 'open'
|
|
209
|
+
|| source.tabId !== state.activeTabId || source.generation !== tab.generation
|
|
210
|
+
|| source.viewportRevision !== state.viewportRevision || source.viewportRevision !== tab.appliedViewportRevision
|
|
211
|
+
|| sourceViewport.width !== viewport.width || sourceViewport.height !== viewport.height
|
|
212
|
+
|| sourceViewport.deviceScaleFactor !== viewport.deviceScaleFactor) {
|
|
213
|
+
throw new BrowserRuntimeError('PROTOCOL_ERROR');
|
|
214
|
+
}
|
|
215
|
+
if (source.frameAlignment !== undefined || source.rtpTimestampFloor !== undefined) {
|
|
216
|
+
if (source.frameAlignment !== 2) throw new BrowserRuntimeError('PROTOCOL_ERROR');
|
|
217
|
+
validateInteger(source.rtpTimestampFloor, 'videoSource.rtpTimestampFloor', 0, 0xffffffff);
|
|
218
|
+
}
|
|
185
219
|
}
|
|
186
220
|
if (state.videoAcceleration !== undefined) {
|
|
187
221
|
const acceleration = validateExactKeys(state.videoAcceleration,
|
|
@@ -12,7 +12,7 @@ const allowedActions = [
|
|
|
12
12
|
'click',
|
|
13
13
|
'fill',
|
|
14
14
|
'press',
|
|
15
|
-
] as const satisfies readonly plugins.
|
|
15
|
+
] as const satisfies readonly plugins.flexharnessTools.TBrowserToolAction[];
|
|
16
16
|
|
|
17
17
|
export class BrowserRuntimeFlexToolProvider<TScope>
|
|
18
18
|
implements plugins.flexharness.IFlexToolProvider<TScope> {
|
|
@@ -67,7 +67,7 @@ implements plugins.flexharness.IFlexToolProvider<TScope> {
|
|
|
67
67
|
}
|
|
68
68
|
this.state = 'active';
|
|
69
69
|
|
|
70
|
-
const tools = plugins.
|
|
70
|
+
const tools = plugins.flexharnessTools.createBrowserTools({
|
|
71
71
|
abortSignal: context.signal,
|
|
72
72
|
browser: this.client.asToolBrowserContext(),
|
|
73
73
|
requestPermission: async (request) => {
|
package/ts/classes.framed.ts
CHANGED
|
@@ -732,7 +732,7 @@ export class BrowserRuntimeFramedClient {
|
|
|
732
732
|
}
|
|
733
733
|
}
|
|
734
734
|
|
|
735
|
-
public asToolBrowserContext(): plugins.
|
|
735
|
+
public asToolBrowserContext(): plugins.flexharnessTools.IToolBrowserContext {
|
|
736
736
|
return {
|
|
737
737
|
execute: async (input, options) => {
|
|
738
738
|
const record = validateExactKeys(
|
|
@@ -6,6 +6,8 @@ const metadataSchema = 1;
|
|
|
6
6
|
const maximumMetadataBytes = 4096;
|
|
7
7
|
const maximumProcFileBytes = 2 * 1024 * 1024;
|
|
8
8
|
const anchorInitializationAttempts = 200;
|
|
9
|
+
const emptyCommandAttempts = 5;
|
|
10
|
+
const emptyCommandRetryDelayMs = 50;
|
|
9
11
|
const generationIdPattern = /^[A-Za-z0-9_-]{32}$/u;
|
|
10
12
|
const hashPattern = /^[a-f0-9]{64}$/u;
|
|
11
13
|
const bootIdPattern = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/u;
|
|
@@ -59,7 +61,26 @@ export interface IBrowserRuntimeGeneration {
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
type TInspectionResult = 'clear' | 'held' | 'indeterminate';
|
|
62
|
-
|
|
64
|
+
/**
|
|
65
|
+
* `same` is every one of the four `/proc/<pid>/status` `Uid:` identifiers, `other` is none of
|
|
66
|
+
* them, and `shared` is a setuid credential transition that names this uid in some but not all
|
|
67
|
+
* of them (`sudo`, `su`, `passwd`, a daemon that assumed this identity). A `shared` process can
|
|
68
|
+
* still open this uid's profile, so it is a scan candidate; it is never a whole-scan fence.
|
|
69
|
+
*/
|
|
70
|
+
type TProcessUidState = 'same' | 'other' | 'shared' | 'missing' | 'indeterminate';
|
|
71
|
+
type TEmptyCommandResolution =
|
|
72
|
+
| { kind: 'clear' }
|
|
73
|
+
| { kind: 'indeterminate' }
|
|
74
|
+
| { kind: 'command'; cmdline: plugins.Buffer };
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Waits for one bounded retry step. The timer is referenced for its whole lifetime and always
|
|
78
|
+
* fires, so it is released as soon as the step ends and never outlives the acquisition it
|
|
79
|
+
* belongs to. An unreferenced timer would let the process exit in the middle of an inspection.
|
|
80
|
+
*/
|
|
81
|
+
const delayStep = (millisecondsArg: number): Promise<void> => new Promise<void>((resolve) => {
|
|
82
|
+
setTimeout(resolve, millisecondsArg);
|
|
83
|
+
});
|
|
63
84
|
|
|
64
85
|
const isMissingError = (errorArg: unknown): boolean => (
|
|
65
86
|
(errorArg as NodeJS.ErrnoException).code === 'ENOENT'
|
|
@@ -1085,9 +1106,13 @@ export class BrowserRuntimeOwnership {
|
|
|
1085
1106
|
}
|
|
1086
1107
|
|
|
1087
1108
|
private async inspectProfileProcesses(profileRootsArg: string[]): Promise<TInspectionResult> {
|
|
1088
|
-
const roots =
|
|
1109
|
+
const roots = await this.presentProfileRoots(profileRootsArg);
|
|
1110
|
+
// The inspection exists only to protect a profile root that is about to be removed. When no
|
|
1111
|
+
// root is present there is nothing to protect and no containment match is possible, so the
|
|
1112
|
+
// process table is never enumerated.
|
|
1113
|
+
if (roots.length === 0) return 'clear';
|
|
1089
1114
|
let indeterminate = false;
|
|
1090
|
-
for (const pid of await this.
|
|
1115
|
+
for (const pid of await this.listCandidateProcessIds()) {
|
|
1091
1116
|
let cmdline: plugins.Buffer;
|
|
1092
1117
|
try {
|
|
1093
1118
|
cmdline = await this.readBoundedBuffer(
|
|
@@ -1095,6 +1120,8 @@ export class BrowserRuntimeOwnership {
|
|
|
1095
1120
|
maximumProcFileBytes,
|
|
1096
1121
|
);
|
|
1097
1122
|
} catch {
|
|
1123
|
+
// A candidate whose command line cannot be read stays fail-closed: its identity is known
|
|
1124
|
+
// to involve this uid and nothing proves it is not using one of these profile roots.
|
|
1098
1125
|
const uidState = await this.processStillExists(pid);
|
|
1099
1126
|
if (uidState === 'missing' || uidState === 'other') continue;
|
|
1100
1127
|
indeterminate = true;
|
|
@@ -1102,37 +1129,66 @@ export class BrowserRuntimeOwnership {
|
|
|
1102
1129
|
}
|
|
1103
1130
|
if (cmdline.byteLength === 0) {
|
|
1104
1131
|
await this.afterEmptyProcessCmdline?.(pid);
|
|
1105
|
-
|
|
1106
|
-
continue;
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
cmdlineText = new TextDecoder('utf-8', { fatal: true }).decode(cmdline);
|
|
1111
|
-
} catch {
|
|
1112
|
-
indeterminate = true;
|
|
1113
|
-
continue;
|
|
1114
|
-
}
|
|
1115
|
-
const argumentsList = cmdlineText.split('\0');
|
|
1116
|
-
if (argumentsList.at(-1) === '') argumentsList.pop();
|
|
1117
|
-
for (let index = 0; index < argumentsList.length; index += 1) {
|
|
1118
|
-
const argument = argumentsList[index]!;
|
|
1119
|
-
let profilePath: string | undefined;
|
|
1120
|
-
if (argument === '--user-data-dir') {
|
|
1121
|
-
profilePath = argumentsList[index + 1];
|
|
1122
|
-
} else if (argument.startsWith('--user-data-dir=')) {
|
|
1123
|
-
profilePath = argument.slice('--user-data-dir='.length);
|
|
1124
|
-
}
|
|
1125
|
-
if (!profilePath || !plugins.path.isAbsolute(profilePath)) continue;
|
|
1126
|
-
const resolved = plugins.path.resolve(profilePath);
|
|
1127
|
-
if (roots.some((root) => resolved === root || this.pathIsWithin(root, resolved))) {
|
|
1128
|
-
return 'held';
|
|
1132
|
+
const resolution = await this.resolveEmptyCommandProcess(pid);
|
|
1133
|
+
if (resolution.kind === 'clear') continue;
|
|
1134
|
+
if (resolution.kind === 'indeterminate') {
|
|
1135
|
+
indeterminate = true;
|
|
1136
|
+
continue;
|
|
1129
1137
|
}
|
|
1138
|
+
cmdline = resolution.cmdline;
|
|
1130
1139
|
}
|
|
1140
|
+
const containment = this.commandLineContainment(cmdline, roots);
|
|
1141
|
+
if (containment === 'held') return 'held';
|
|
1142
|
+
if (containment === 'indeterminate') indeterminate = true;
|
|
1131
1143
|
}
|
|
1132
1144
|
return indeterminate ? 'indeterminate' : 'clear';
|
|
1133
1145
|
}
|
|
1134
1146
|
|
|
1135
|
-
private
|
|
1147
|
+
private commandLineContainment(
|
|
1148
|
+
cmdlineArg: plugins.Buffer,
|
|
1149
|
+
rootsArg: string[],
|
|
1150
|
+
): TInspectionResult {
|
|
1151
|
+
let cmdlineText: string;
|
|
1152
|
+
try {
|
|
1153
|
+
cmdlineText = new TextDecoder('utf-8', { fatal: true }).decode(cmdlineArg);
|
|
1154
|
+
} catch {
|
|
1155
|
+
return 'indeterminate';
|
|
1156
|
+
}
|
|
1157
|
+
const argumentsList = cmdlineText.split('\0');
|
|
1158
|
+
if (argumentsList.at(-1) === '') argumentsList.pop();
|
|
1159
|
+
for (let index = 0; index < argumentsList.length; index += 1) {
|
|
1160
|
+
const argument = argumentsList[index]!;
|
|
1161
|
+
let profilePath: string | undefined;
|
|
1162
|
+
if (argument === '--user-data-dir') {
|
|
1163
|
+
profilePath = argumentsList[index + 1];
|
|
1164
|
+
} else if (argument.startsWith('--user-data-dir=')) {
|
|
1165
|
+
profilePath = argument.slice('--user-data-dir='.length);
|
|
1166
|
+
}
|
|
1167
|
+
if (!profilePath || !plugins.path.isAbsolute(profilePath)) continue;
|
|
1168
|
+
const resolved = plugins.path.resolve(profilePath);
|
|
1169
|
+
if (rootsArg.some((root) => resolved === root || this.pathIsWithin(root, resolved))) {
|
|
1170
|
+
return 'held';
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return 'clear';
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
private async presentProfileRoots(profileRootsArg: string[]): Promise<string[]> {
|
|
1177
|
+
const roots: string[] = [];
|
|
1178
|
+
for (const profileRoot of profileRootsArg) {
|
|
1179
|
+
const root = plugins.path.resolve(profileRoot);
|
|
1180
|
+
try {
|
|
1181
|
+
await plugins.fsPromises.lstat(root, { bigint: true });
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
if (isMissingError(error)) continue;
|
|
1184
|
+
throw new BrowserRuntimeError('FENCED', 'runtime profile root is unreadable');
|
|
1185
|
+
}
|
|
1186
|
+
roots.push(root);
|
|
1187
|
+
}
|
|
1188
|
+
return roots;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
private async listCandidateProcessIds(): Promise<number[]> {
|
|
1136
1192
|
let entries: string[];
|
|
1137
1193
|
try {
|
|
1138
1194
|
// /proc can report DT_UNKNOWN. Node's eager Dirent conversion then
|
|
@@ -1161,8 +1217,11 @@ export class BrowserRuntimeOwnership {
|
|
|
1161
1217
|
indeterminate = true;
|
|
1162
1218
|
continue;
|
|
1163
1219
|
}
|
|
1220
|
+
// A credential state alone never fences the scan. An unrelated process is discarded here
|
|
1221
|
+
// only when it definitely belongs to another uid; every process this uid owns or shares is
|
|
1222
|
+
// a candidate whose command line decides it in inspectProfileProcesses().
|
|
1164
1223
|
const uidState = await this.processStillExists(pid);
|
|
1165
|
-
if (uidState === 'same') processIds.push(pid);
|
|
1224
|
+
if (uidState === 'same' || uidState === 'shared') processIds.push(pid);
|
|
1166
1225
|
else if (uidState === 'indeterminate') indeterminate = true;
|
|
1167
1226
|
}
|
|
1168
1227
|
if (indeterminate) {
|
|
@@ -1208,31 +1267,63 @@ export class BrowserRuntimeOwnership {
|
|
|
1208
1267
|
}
|
|
1209
1268
|
if (ids.every((id) => id === this.uid)) return 'same';
|
|
1210
1269
|
if (ids.every((id) => id !== this.uid)) return 'other';
|
|
1211
|
-
return '
|
|
1270
|
+
return 'shared';
|
|
1212
1271
|
}
|
|
1213
1272
|
|
|
1214
|
-
|
|
1273
|
+
/**
|
|
1274
|
+
* An empty `/proc/<pid>/cmdline` is not a decision. A candidate publishes one for a bounded
|
|
1275
|
+
* window while it is mid-`execve` or has already released its mm on the way to becoming a
|
|
1276
|
+
* zombie, and during that window `State:` still reports a running process. Re-read the
|
|
1277
|
+
* identity and the command line across that window before deciding anything.
|
|
1278
|
+
*/
|
|
1279
|
+
private async resolveEmptyCommandProcess(pidArg: number): Promise<TEmptyCommandResolution> {
|
|
1215
1280
|
const processPath = plugins.path.join(this.procRoot, String(pidArg));
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1281
|
+
for (let attempt = 0; attempt < emptyCommandAttempts; attempt += 1) {
|
|
1282
|
+
let status: string;
|
|
1283
|
+
try {
|
|
1284
|
+
status = await this.readBoundedFile(
|
|
1285
|
+
plugins.path.join(processPath, 'status'),
|
|
1286
|
+
64 * 1024,
|
|
1287
|
+
);
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
return await this.departedProcessResolution(processPath, error);
|
|
1290
|
+
}
|
|
1221
1291
|
const uidState = this.parseProcessUidState(status);
|
|
1222
|
-
if (uidState === 'other') return
|
|
1223
|
-
if (uidState !== 'same') return
|
|
1292
|
+
if (uidState === 'other') return { kind: 'clear' };
|
|
1293
|
+
if (uidState !== 'same' && uidState !== 'shared') return { kind: 'indeterminate' };
|
|
1224
1294
|
const stateLines = status.split('\n').filter((line) => line.startsWith('State:'));
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
if (
|
|
1228
|
-
|
|
1229
|
-
// alone is insufficient: the PID directory must also have disappeared.
|
|
1295
|
+
if (stateLines.length !== 1) return { kind: 'indeterminate' };
|
|
1296
|
+
// A zombie has released its mm, holds no descriptor and cannot be using any profile root.
|
|
1297
|
+
if (/^State:[\t ]+Z(?:[\t ]|$)/u.test(stateLines[0]!)) return { kind: 'clear' };
|
|
1298
|
+
let cmdline: plugins.Buffer;
|
|
1230
1299
|
try {
|
|
1231
|
-
await
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1300
|
+
cmdline = await this.readBoundedBuffer(
|
|
1301
|
+
plugins.path.join(processPath, 'cmdline'),
|
|
1302
|
+
maximumProcFileBytes,
|
|
1303
|
+
);
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
return await this.departedProcessResolution(processPath, error);
|
|
1235
1306
|
}
|
|
1307
|
+
if (cmdline.byteLength > 0) return { kind: 'command', cmdline };
|
|
1308
|
+
if (attempt + 1 < emptyCommandAttempts) await delayStep(emptyCommandRetryDelayMs);
|
|
1309
|
+
}
|
|
1310
|
+
// Still running and still command-less after the whole window. Nothing proves this process is
|
|
1311
|
+
// not using one of the inspected roots, so it stays fail-closed.
|
|
1312
|
+
return { kind: 'indeterminate' };
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
private async departedProcessResolution(
|
|
1316
|
+
processPathArg: string,
|
|
1317
|
+
errorArg: unknown,
|
|
1318
|
+
): Promise<TEmptyCommandResolution> {
|
|
1319
|
+
if (!isMissingError(errorArg)) return { kind: 'indeterminate' };
|
|
1320
|
+
// A missing status or command-line file alone is insufficient: the PID directory must also
|
|
1321
|
+
// have disappeared before the process counts as departed.
|
|
1322
|
+
try {
|
|
1323
|
+
await plugins.fsPromises.lstat(processPathArg, { bigint: true });
|
|
1324
|
+
return { kind: 'indeterminate' };
|
|
1325
|
+
} catch (statError) {
|
|
1326
|
+
return isMissingError(statError) ? { kind: 'clear' } : { kind: 'indeterminate' };
|
|
1236
1327
|
}
|
|
1237
1328
|
}
|
|
1238
1329
|
|
package/ts/plugins.ts
CHANGED
|
@@ -16,12 +16,12 @@ export { Buffer, crypto, dns, fs, fsPromises, http, net, os, path, stream, tls,
|
|
|
16
16
|
|
|
17
17
|
// foss.global scopes
|
|
18
18
|
import * as flexharness from '@modelprofile.com/flexharness';
|
|
19
|
-
import * as
|
|
19
|
+
import * as flexharnessTools from '@modelprofile.com/flexharness/tools';
|
|
20
20
|
import * as smartipc from '@push.rocks/smartipc';
|
|
21
21
|
import * as smartmcp from '@push.rocks/smartmcp';
|
|
22
22
|
import * as smartpuppeteer from '@push.rocks/smartpuppeteer';
|
|
23
23
|
|
|
24
|
-
export { flexharness,
|
|
24
|
+
export { flexharness, flexharnessTools, smartipc, smartmcp, smartpuppeteer };
|
|
25
25
|
|
|
26
26
|
// third-party scope
|
|
27
27
|
import ipaddr from 'ipaddr.js';
|
package/.smartconfig.json
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"@git.zone/cli": {
|
|
3
|
-
"schemaVersion": 2,
|
|
4
|
-
"projectType": "npm",
|
|
5
|
-
"module": {
|
|
6
|
-
"githost": "code.foss.global",
|
|
7
|
-
"gitscope": "modelprofile.com",
|
|
8
|
-
"gitrepo": "browser-runtime",
|
|
9
|
-
"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.",
|
|
10
|
-
"npmPackagename": "@modelprofile.com/browser-runtime",
|
|
11
|
-
"license": "MIT",
|
|
12
|
-
"projectDomain": "modelprofile.com"
|
|
13
|
-
},
|
|
14
|
-
"release": {
|
|
15
|
-
"targets": {
|
|
16
|
-
"git": {
|
|
17
|
-
"enabled": true,
|
|
18
|
-
"remote": "origin",
|
|
19
|
-
"pushBranch": true,
|
|
20
|
-
"pushTags": true
|
|
21
|
-
},
|
|
22
|
-
"npm": {
|
|
23
|
-
"enabled": true,
|
|
24
|
-
"registries": [
|
|
25
|
-
"https://verdaccio.lossless.digital",
|
|
26
|
-
"https://registry.npmjs.org"
|
|
27
|
-
],
|
|
28
|
-
"accessLevel": "public",
|
|
29
|
-
"alreadyPublished": "success"
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|