@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/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 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.
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
- 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 no same-UID 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`.
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
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/browser-runtime',
6
- version: '5.7.1',
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.smartagent.TBrowserToolAction[];
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.smartagent.createBrowserTools({
70
+ const tools = plugins.flexharnessTools.createBrowserTools({
71
71
  abortSignal: context.signal,
72
72
  browser: this.client.asToolBrowserContext(),
73
73
  requestPermission: async (request) => {
@@ -732,7 +732,7 @@ export class BrowserRuntimeFramedClient {
732
732
  }
733
733
  }
734
734
 
735
- public asToolBrowserContext(): plugins.smartagent.IToolBrowserContext {
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
- type TProcessUidState = 'same' | 'other' | 'missing' | 'indeterminate';
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 = profileRootsArg.map((root) => plugins.path.resolve(root));
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.listOwnedProcessIds()) {
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
- if (!await this.emptyCommandProcessIsClear(pid)) indeterminate = true;
1106
- continue;
1107
- }
1108
- let cmdlineText: string;
1109
- try {
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 async listOwnedProcessIds(): Promise<number[]> {
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 'indeterminate';
1270
+ return 'shared';
1212
1271
  }
1213
1272
 
1214
- private async emptyCommandProcessIsClear(pidArg: number): Promise<boolean> {
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
- try {
1217
- const status = await this.readBoundedFile(
1218
- plugins.path.join(processPath, 'status'),
1219
- 64 * 1024,
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 true;
1223
- if (uidState !== 'same') return false;
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
- return stateLines.length === 1 && /^State:[\t ]+Z(?:[\t ]|$)/u.test(stateLines[0]!);
1226
- } catch (error) {
1227
- if (!isMissingError(error)) return false;
1228
- // An empty command line can precede process exit. A missing status file
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 plugins.fsPromises.lstat(processPath, { bigint: true });
1232
- return false;
1233
- } catch (statError) {
1234
- return isMissingError(statError);
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 smartagent from '@push.rocks/smartagent';
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, smartagent, smartipc, smartmcp, smartpuppeteer };
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
- }