@kingsoftcloud/agentkit-harness-node 0.0.0-stage → 0.0.1

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.
Files changed (151) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +1 -0
  3. package/README.md +124 -2
  4. package/THIRD_PARTY_NOTICES.md +66 -0
  5. package/ZCode-LICENSE +202 -0
  6. package/dist/index.mjs +4871 -0
  7. package/dist/native-command-plan.LICENSE.txt +221 -0
  8. package/dist/native-command-plan.json +16 -0
  9. package/dist/native-command-plan.mjs +8 -0
  10. package/dist/rpc-entry.mjs +5682 -0
  11. package/package.json +169 -4
  12. package/src/native/a2a-receipt-source.mjs +19 -0
  13. package/src/native/a2a-tools.mjs +517 -0
  14. package/src/native/activity-status.mjs +30 -0
  15. package/src/native/artifact-codec.mjs +481 -0
  16. package/src/native/artifact-store.mjs +1162 -0
  17. package/src/native/assistant-stream.mjs +76 -0
  18. package/src/native/background-command-port.mjs +95 -0
  19. package/src/native/browser-action-observation.mjs +43 -0
  20. package/src/native/browser-availability.mjs +22 -0
  21. package/src/native/browser-canvas-metrics.mjs +71 -0
  22. package/src/native/browser-dom.mjs +193 -0
  23. package/src/native/browser-playwright.mjs +1249 -0
  24. package/src/native/browser-vision.mjs +369 -0
  25. package/src/native/browser-workspace-policy.mjs +22 -0
  26. package/src/native/browser-workspace-protocol.mjs +95 -0
  27. package/src/native/browser-workspace-server.mjs +164 -0
  28. package/src/native/browser-workspace.mjs +355 -0
  29. package/src/native/captured-source.mjs +3 -0
  30. package/src/native/child-budget-allocation.mjs +87 -0
  31. package/src/native/child-capabilities.mjs +35 -0
  32. package/src/native/child-command-resource.mjs +20 -0
  33. package/src/native/child-history.mjs +188 -0
  34. package/src/native/child-merge.mjs +98 -0
  35. package/src/native/child-model-surface.mjs +19 -0
  36. package/src/native/child-observation.mjs +100 -0
  37. package/src/native/child-owner.mjs +620 -0
  38. package/src/native/child-publication.mjs +76 -0
  39. package/src/native/child-readers.mjs +366 -0
  40. package/src/native/child-tools.mjs +46 -0
  41. package/src/native/child-workspace-binding.mjs +29 -0
  42. package/src/native/child-workspace-tools.mjs +1 -0
  43. package/src/native/child-workspaces.mjs +655 -0
  44. package/src/native/children.mjs +365 -0
  45. package/src/native/command-plan-worker.mjs +14 -0
  46. package/src/native/command-plan.mjs +31 -0
  47. package/src/native/command-progress.mjs +64 -0
  48. package/src/native/command-shell.mjs +9 -0
  49. package/src/native/command-workspace-observation.mjs +36 -0
  50. package/src/native/component-tools.mjs +280 -0
  51. package/src/native/context-counter.mjs +133 -0
  52. package/src/native/context-markers.mjs +51 -0
  53. package/src/native/context-profile.mjs +72 -0
  54. package/src/native/context-status.mjs +27 -0
  55. package/src/native/conversation-command-control.mjs +133 -0
  56. package/src/native/delivery-timeline.mjs +39 -0
  57. package/src/native/dsh-background-ownership.mjs +102 -0
  58. package/src/native/dsh-managed-ownership.mjs +23 -0
  59. package/src/native/dsh-turn-contract.mjs +3 -0
  60. package/src/native/dsh-turn-ownership.mjs +54 -0
  61. package/src/native/dsh-turn-runtime.mjs +498 -0
  62. package/src/native/e2b-background-supervisor.mjs +347 -0
  63. package/src/native/e2b-background.mjs +368 -0
  64. package/src/native/e2b-command-state.mjs +40 -0
  65. package/src/native/e2b-command-tools.mjs +486 -0
  66. package/src/native/e2b-sandbox-owner.mjs +227 -0
  67. package/src/native/hook-executor.mjs +249 -0
  68. package/src/native/instruction-assembly.mjs +60 -0
  69. package/src/native/instruction-context.mjs +54 -0
  70. package/src/native/local-background-supervisor.mjs +108 -0
  71. package/src/native/local-background.mjs +207 -0
  72. package/src/native/local-command-evidence.mjs +57 -0
  73. package/src/native/local-command-policy-worker.mjs +197 -0
  74. package/src/native/local-command-policy.mjs +46 -0
  75. package/src/native/local-command-state.mjs +177 -0
  76. package/src/native/local-command-tools.mjs +302 -0
  77. package/src/native/local-execution-profile.mjs +36 -0
  78. package/src/native/local-output-policy.mjs +92 -0
  79. package/src/native/local-process-bin/darwin-arm64/launcher +0 -0
  80. package/src/native/local-process-bin/darwin-x64/launcher +0 -0
  81. package/src/native/local-process-bin/linux-arm64/launcher +0 -0
  82. package/src/native/local-process-bin/linux-x64/launcher +0 -0
  83. package/src/native/local-process-launcher.c +183 -0
  84. package/src/native/local-process-licenses/GCC-COPYING.RUNTIME +72 -0
  85. package/src/native/local-process-licenses/GCC-COPYING3 +674 -0
  86. package/src/native/local-process-licenses/NOTICE +21 -0
  87. package/src/native/local-process-licenses/musl-COPYRIGHT +193 -0
  88. package/src/native/local-process-manifest.json +32 -0
  89. package/src/native/local-process.mjs +246 -0
  90. package/src/native/local-resource-profile.mjs +32 -0
  91. package/src/native/memory-candidate.mjs +121 -0
  92. package/src/native/memory-extraction-run.mjs +60 -0
  93. package/src/native/memory-extraction-tools.mjs +116 -0
  94. package/src/native/memory-recall.mjs +65 -0
  95. package/src/native/memory-store.mjs +334 -0
  96. package/src/native/memory-tools.mjs +329 -0
  97. package/src/native/model-anthropic-cache.mjs +49 -0
  98. package/src/native/model-anthropic-tool-search.mjs +85 -0
  99. package/src/native/model-anthropic.mjs +753 -0
  100. package/src/native/model-chat-usage.mjs +24 -0
  101. package/src/native/model-inline-reasoning.mjs +111 -0
  102. package/src/native/model-media.mjs +174 -0
  103. package/src/native/model-normalize.mjs +228 -0
  104. package/src/native/model-openai-chat.mjs +561 -0
  105. package/src/native/model-port.mjs +361 -0
  106. package/src/native/model-provider-compat.mjs +129 -0
  107. package/src/native/model-replay.mjs +153 -0
  108. package/src/native/model-request.mjs +15 -0
  109. package/src/native/model-responses.mjs +1205 -0
  110. package/src/native/model-sse.mjs +171 -0
  111. package/src/native/model-surface-host.mjs +27 -0
  112. package/src/native/model-tool-search-policy.mjs +16 -0
  113. package/src/native/model-transport-observation.mjs +27 -0
  114. package/src/native/owned-child.mjs +114 -0
  115. package/src/native/owned-command.mjs +106 -0
  116. package/src/native/python-syntax-worker.mjs +60 -0
  117. package/src/native/python-syntax.mjs +50 -0
  118. package/src/native/read-interruption.mjs +16 -0
  119. package/src/native/result-stream.mjs +63 -0
  120. package/src/native/ruff-ast.mjs +95 -0
  121. package/src/native/run-observation.mjs +128 -0
  122. package/src/native/session-delivery-journal.mjs +96 -0
  123. package/src/native/session-delivery.mjs +434 -0
  124. package/src/native/shell-analysis.mjs +50 -0
  125. package/src/native/skill-disclosure.mjs +404 -0
  126. package/src/native/skill-events.mjs +164 -0
  127. package/src/native/skill-tools.mjs +813 -0
  128. package/src/native/source-page.mjs +73 -0
  129. package/src/native/sqlite-session-inbox.mjs +42 -0
  130. package/src/native/sqlite-store.mjs +617 -0
  131. package/src/native/tool-authority.mjs +99 -0
  132. package/src/native/tool-guidance.mjs +12 -0
  133. package/src/native/tool-observation.mjs +75 -0
  134. package/src/native/tool-states.mjs +14 -0
  135. package/src/native/verification-authority.mjs +37 -0
  136. package/src/native/web-tools.mjs +643 -0
  137. package/src/native/workspace-checkpoints.mjs +312 -0
  138. package/src/native/workspace-context.mjs +61 -0
  139. package/src/native/workspace-display.mjs +39 -0
  140. package/src/native/workspace-edit-match.mjs +19 -0
  141. package/src/native/workspace-edit-worker.mjs +15 -0
  142. package/src/native/workspace-editing.mjs +447 -0
  143. package/src/native/workspace-image.mjs +72 -0
  144. package/src/native/workspace-instructions.mjs +107 -0
  145. package/src/native/workspace-mutation-lock.mjs +74 -0
  146. package/src/native/workspace-patch-worker.mjs +17 -0
  147. package/src/native/workspace-patch.mjs +17 -0
  148. package/src/native/workspace-paths.mjs +225 -0
  149. package/src/native/workspace-state.mjs +272 -0
  150. package/src/native/workspace-subject-observer.mjs +84 -0
  151. package/src/native/workspace-tools.mjs +1740 -0
@@ -0,0 +1,355 @@
1
+ /** Studio's run-owned browser capability. Backend and authorization are Host supplied. */
2
+ import { createHash } from 'node:crypto';
3
+ import { mkdirSync, realpathSync } from 'node:fs';
4
+ import { extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
+ import { setTimeout as delay } from 'node:timers/promises';
6
+ import { canonical, scopeKey, matchesToolExecutionContract } from '@kingsoftcloud/agentkit-harness-core';
7
+ import { validateBrowserAction, validateBrowserObservation, validateBrowserRecovery, validateBrowserSessionRef,
8
+ browserObservationDigest } from '@kingsoftcloud/agentkit-harness-core/browser-contract';
9
+ import { createBrowserWorkspaceServer, resolveBrowserWorkspaceEntry } from './browser-workspace-server.mjs';
10
+ import { parseWorkspaceBrowserInput, studioBrowserDefinition } from './browser-workspace-protocol.mjs';
11
+ import { publishWorkspaceBytes, realpathExisting } from './workspace-paths.mjs';
12
+ import { admitBrowserActionObservation } from './browser-action-observation.mjs';
13
+ import { WORKSPACE_BROWSER_BOUNDARY, workspaceBrowserCapability } from './browser-workspace-policy.mjs';
14
+
15
+ const hash = value => createHash('sha256').update(typeof value === 'string' || Buffer.isBuffer(value) ? value : canonical(value)).digest('hex');
16
+ const same = (left, right) => canonical(left) === canonical(right);
17
+ const within = (root, path) => path === root || path.startsWith(root.endsWith(sep) ? root : root + sep);
18
+ const fail = code => { const error = new Error(code); error.code = code; throw error; };
19
+ const pointerWarning = 'The root document of this element is not valid for pointer lock.';
20
+ const warning = value => [pointerWarning, `error: ${pointerWarning}`].includes(value.trim());
21
+ const safeInput = new Set(['workspace_path_invalid', 'workspace_path_outside_scope', 'browser_entry_not_found',
22
+ 'screenshot_path_invalid', 'browser_actions_invalid', 'browser_action_invalid', 'browser_input_invalid',
23
+ 'browser_wait_invalid', 'browser_action_time_budget_exceeded', 'browser_served_file_limit_exceeded']);
24
+
25
+ function screenshotTarget(root, value) {
26
+ const target = realpathExisting(resolve(root, value));
27
+ if (!within(root, target) || extname(target).toLowerCase() !== '.png') fail('screenshot_path_invalid');
28
+ return target;
29
+ }
30
+ function launchLabel(value) {
31
+ let label = value.trim();
32
+ for (let count = 0; count < 4 && /^[\p{P}\p{S}]/u.test(label); count++) label = [...label].slice(1).join('').trimStart();
33
+ label = label.replace(/\s+/gu, ' ').toLowerCase();
34
+ return ['start', 'start game', 'new game', 'play', '开始'].includes(label)
35
+ || /^(?:新(?:的|建)?|开始(?:新(?:的|建)?)?|进入)(?:游戏|世界)$/u.test(label)
36
+ || /^进入(?!设置|帮助|选项|商店|社区|账号|登录|房间|服务器)[\u4e00-\u9fff]{1,10}(?:\s*[▶→])?$/u.test(label);
37
+ }
38
+
39
+ export async function createStudioBrowserWorkspacePort({ scope: rawScope, root, runtimeId, createBrowser,
40
+ reobserveAfterLoss = false, evidenceRoot, serverFactory = createBrowserWorkspaceServer,
41
+ toolTimeoutMs = 110000,
42
+ sleep = (ms, signal) => delay(ms, undefined, { signal }) } = {}) {
43
+ scopeKey(rawScope);
44
+ if (!Number.isSafeInteger(toolTimeoutMs) || toolTimeoutMs < 110000) fail('browser_owner_timeout_invalid');
45
+ const definition = { ...studioBrowserDefinition(), timeoutMs: toolTimeoutMs };
46
+ if (typeof reobserveAfterLoss !== 'boolean' || typeof createBrowser !== 'function'
47
+ || typeof runtimeId !== 'string' || !runtimeId.trim() || !isAbsolute(root ?? '')) fail('browser_owner_configuration_invalid');
48
+ const scope = structuredClone(rawScope), workspace = realpathSync(root), workspaceRevision = hash(workspace);
49
+ let evidence;
50
+ if (evidenceRoot !== undefined) {
51
+ if (typeof evidenceRoot !== 'string' || !isAbsolute(evidenceRoot)) fail('browser_verification_evidence_unavailable');
52
+ evidence = realpathExisting(evidenceRoot);
53
+ if (within(workspace, evidence)) fail('browser_verification_evidence_must_be_outside_workspace');
54
+ mkdirSync(evidence, { recursive: true }); evidence = realpathSync(evidence);
55
+ }
56
+ const pages = new Map(), lost = new Map(), locks = new Map(), targets = new Map(), retired = new Set();
57
+ const lifetime = new AbortController(); let closed = false, closing, verificationAttempt = 0;
58
+ const owns = ref => same(ref.scope, scope) && ref.runtimeId === runtimeId && ref.workspaceRevision === workspaceRevision;
59
+ const browser = await createBrowser({ scope: structuredClone(scope), runtimeId, workspaceRevision,
60
+ viewport: [...WORKSPACE_BROWSER_BOUNDARY.viewport],
61
+ networkPolicy(ref, url) {
62
+ const page = pages.get(ref.runId), target = targets.get(ref.runId);
63
+ let current = true;
64
+ try { current = target?.assertCurrent?.() !== false; } catch { current = false; }
65
+ return current && !closed && !!page && !page.revoked && same(page.ref, ref) && page.server.allows(url);
66
+ },
67
+ artifactSink(ref, payload, mime) {
68
+ const page = pages.get(ref.runId), target = targets.get(ref.runId);
69
+ if (closed || !page || page.revoked || !same(page.ref, ref) || !owns(ref) || !target || target.signal.aborted || mime !== 'image/png'
70
+ || !(payload instanceof Uint8Array)) fail('screenshot_target_unavailable');
71
+ if (target.assertCurrent && target.assertCurrent() !== true) fail('browser_authority_stale');
72
+ const boundedRoot = target.evidence ? evidence : workspace;
73
+ if (!boundedRoot || !within(boundedRoot, realpathExisting(target.path))) fail('screenshot_path_invalid');
74
+ publishWorkspaceBytes(target.path, Buffer.from(payload));
75
+ if (!target.evidence) page.server.markGenerated(target.path);
76
+ return `sha256:${hash(Buffer.from(payload))}`;
77
+ },
78
+ });
79
+ for (const name of ['open', 'act', 'recover', 'restart', 'close']) if (typeof browser?.[name] !== 'function') fail('browser_backend_contract_invalid');
80
+
81
+ const combine = signal => signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
82
+ async function serialMany(runIds, signal, callback) {
83
+ if (closed) fail('browser_owner_closed');
84
+ if (runIds.some(runId => typeof runId !== 'string' || !runId.trim())) fail('browser_run_identity_invalid');
85
+ const ids = [...new Set(runIds)], previous = ids.map(runId => locks.get(runId) ?? Promise.resolve());
86
+ let release; const barrier = new Promise(resolve => { release = resolve; });
87
+ for (const runId of ids) locks.set(runId, barrier);
88
+ await Promise.all(previous);
89
+ try { if (closed) fail('browser_owner_closed'); signal.throwIfAborted(); return await callback(); }
90
+ finally { release(); for (const runId of ids) if (locks.get(runId) === barrier) locks.delete(runId); }
91
+ }
92
+ const serial = (runId, signal, callback) => serialMany([runId], signal, callback);
93
+ async function closePage(runId, { closeBrowser = true, signal, confirmClosed = false } = {}) {
94
+ const page = pages.get(runId); pages.delete(runId); targets.delete(runId);
95
+ if (!page) return;
96
+ try { if (closeBrowser) await browser.close(page.ref, { signal, confirmClosed }); }
97
+ finally { await page.server.close(); }
98
+ }
99
+ async function openPage(runId, input, operationKey, signal, prior) {
100
+ const server = await serverFactory({ root: workspace, path: input.path, signal });
101
+ try {
102
+ const ref = prior ? validateBrowserRecovery(await browser.restart(prior, { signal })).sessionRef
103
+ : validateBrowserSessionRef(await browser.open(runId, { operationKey, signal }));
104
+ if (!owns(ref) || ref.runId !== runId || prior && !same(ref, { ...prior,
105
+ generation: prior.generation + 1, leaseEpoch: prior.leaseEpoch + 1 })) fail('browser_backend_identity_mismatch');
106
+ const page = { server, ref, sourceRevision: server.initialRevision, actionObservations: new Map() };
107
+ pages.set(runId, page); return page;
108
+ } catch (error) { await server.close(); throw error; }
109
+ }
110
+ async function act(page, operationKey, index, kind, signal, arguments_ = {}, executeAction) {
111
+ signal.throwIfAborted();
112
+ const action = validateBrowserAction({ actionId: `${operationKey}:${index}`, kind,
113
+ timeoutMs: ['navigate', 'click', 'fill', 'press', 'move'].includes(kind) ? 30000 : 20000,
114
+ ...(kind === 'navigate' ? { url: page.server.url } : {}), ...arguments_ });
115
+ const dispatch = actionSignal => browser.act(page.ref, action, {
116
+ signal: actionSignal ? AbortSignal.any([signal, actionSignal]) : signal,
117
+ observeAction(value) {
118
+ const fact = admitBrowserActionObservation(value, action);
119
+ if (!fact || closed || page.revoked) return;
120
+ page.actionObservations.set(action.actionId, fact);
121
+ while (page.actionObservations.size > 32) page.actionObservations.delete(page.actionObservations.keys().next().value);
122
+ },
123
+ });
124
+ const observation = validateBrowserObservation(await (executeAction ? executeAction({
125
+ entryPath: relative(workspace, page.server.entry).split(sep).join('/'), action: structuredClone(action),
126
+ }, dispatch) : dispatch()));
127
+ if (!same(observation.sessionRef, page.ref) || observation.actionId !== action.actionId) fail('browser_backend_identity_mismatch');
128
+ return observation;
129
+ }
130
+ async function result(observations, page, target, actions, navigationReason, request) {
131
+ const latest = observations.at(-1), all = key => observations.flatMap(row => row[key]);
132
+ const consoleErrors = all('console').filter(value => /^(error:|assert:)/.test(value));
133
+ const pageErrors = all('pageErrors'), networkFailures = all('networkFailures');
134
+ const rendered = !!(latest.text || latest.controls.length || latest.screenshot);
135
+ const ok = observations.every(row => row.status === 'ok' && row.complete)
136
+ && (latest.statusCode === null || latest.statusCode < 400) && rendered
137
+ && ![...consoleErrors, ...pageErrors].some(value => !warning(value)) && !networkFailures.length
138
+ && actions.every(row => row.ok);
139
+ const actionObservations = observations.flatMap(row => page.actionObservations.get(row.actionId) ?? []);
140
+ const diagnostic = actionObservations.findLast(row => row.outcome !== 'ok');
141
+ const content = { ok, browser_channel: browser.browserChannel ?? 'unknown', browser_version: browser.browserVersion ?? 'unknown',
142
+ ...(diagnostic ? { browser_action_diagnostic: { phase: diagnostic.phase, outcome: diagnostic.outcome,
143
+ dispatch_started: diagnostic.dispatchStarted, dispatch_completed: diagnostic.dispatchCompleted, elapsed_ms: diagnostic.elapsedMs } } : {}),
144
+ entry_path: relative(workspace, page.server.entry).split(sep).join('/'), page_navigation_reason: navigationReason,
145
+ title: latest.title, status_code: latest.statusCode, rendered, body_text_preview: [...latest.text].slice(0, 2000).join(''),
146
+ dom_changed_during_capture: latest.domChangedDuringCapture, scroll_x: latest.scrollX, scroll_y: latest.scrollY,
147
+ console_errors: consoleErrors.filter(value => !warning(value)).slice(0, 20), page_errors: pageErrors.filter(value => !warning(value)).slice(0, 20),
148
+ page_error_locations: all('pageErrorLocations').slice(0, 20), environment_warnings: [...consoleErrors, ...pageErrors].filter(warning).slice(0, 20),
149
+ request_failures: networkFailures.slice(0, 20), actions_ok: actions.every(row => row.ok), actions,
150
+ browser_session_id: latest.sessionRef.sessionId, browser_action_seq: latest.actionSeq, error_code: latest.errorCode,
151
+ pointer_lock_active: latest.pointerLockActive, controls: latest.controls.slice(0, 20).map(control => ({
152
+ tag: control.tag, role: control.role, label: control.label, selector: control.selector, element_ref: control.elementRef,
153
+ clickable: control.clickable, hit_test_reachable: control.hitTestReachable, duplicate_id: control.duplicateId,
154
+ })), ...(latest.screenshot ? { screenshot_path: relative(workspace, target).split(sep).join('/') } : {}) };
155
+ const uncertain = observations.some(row => row.status === 'uncertain');
156
+ const browserIsolation = uncertain ? await isolateUnknownPage(page, request) : undefined;
157
+ if (browserIsolation) content.resource_continuity = isolationObservation();
158
+ return { status: uncertain ? 'uncertain' : ok ? 'ok' : 'error', content,
159
+ ...(browserIsolation ? { browserIsolation } : {}),
160
+ browserSessionRef: structuredClone(latest.sessionRef), browserObservation: structuredClone(latest),
161
+ metadata: { progressObservationDigest: browserObservationDigest(latest, hash),
162
+ ...(actionObservations.length ? { browserActionObservations: actionObservations } : {}) },
163
+ context: { content: canonical(content), ...(latest.screenshot ? { media: [structuredClone(latest.screenshot.contentBlock)] } : {}) } };
164
+ }
165
+
166
+ const isolationObservation = () => ({ status: 'lost', original_action_outcome: 'unknown',
167
+ old_page_fenced: true, old_actions_replayed: false, fresh_observation_required: true,
168
+ required_actions: [{ type: 'reload' }] });
169
+ async function isolateUnknownPage(page, request) {
170
+ page.revoked = true; lost.set(page.ref.runId, structuredClone(page.ref));
171
+ if (!reobserveAfterLoss || request.definition.recoveryPolicy !== 'ephemeral-browser/v1') return;
172
+ try {
173
+ const signal = combine(request.signal); signal.throwIfAborted();
174
+ if (request.assertCurrent && request.assertCurrent() !== true) return;
175
+ await closePage(page.ref.runId, { signal, confirmClosed: true });
176
+ signal.throwIfAborted();
177
+ if (request.assertCurrent && request.assertCurrent() !== true) return;
178
+ return { contract: 'agentkit.browser-isolation/v1', sessionRef: structuredClone(page.ref),
179
+ operationKey: request.operationKey, fenced: true, externalEffects: false, oldActionsReplayed: false };
180
+ } catch { /* Without completed owner cleanup, uncertainty still stops the Run. */ }
181
+ }
182
+
183
+ return {
184
+ get capability() { return workspaceBrowserCapability(); },
185
+ get definition() { return structuredClone(definition); },
186
+ async execute(request) {
187
+ if (!same(request.scope, scope) || request.call?.name !== 'browser_inspect_workspace'
188
+ || !matchesToolExecutionContract(request.definition, definition)) return { status: 'error', content: { code: 'scope_or_tool_mismatch', executed: false } };
189
+ const signal = combine(request.signal); signal.throwIfAborted();
190
+ const checkOwned = () => {
191
+ signal.throwIfAborted();
192
+ if (request.assertCurrent && request.assertCurrent() !== true) fail('browser_authority_stale');
193
+ return true;
194
+ };
195
+ checkOwned();
196
+ let input, entry, target;
197
+ try {
198
+ input = parseWorkspaceBrowserInput(request.call.arguments, request.operationKey);
199
+ entry = resolveBrowserWorkspaceEntry(workspace, input.path).entry;
200
+ target = screenshotTarget(workspace, input.screenshotPath);
201
+ } catch (error) { return { status: 'error', content: { code: safeInput.has(error.code) ? error.code : 'browser_entry_not_found' } }; }
202
+ return serial(request.runId, signal, async () => {
203
+ checkOwned();
204
+ if (retired.has(request.runId)) return { status: 'error', content: { code: 'browser_run_retired', executed: false } };
205
+ const prior = lost.get(request.runId);
206
+ if (prior && (input.actions.length !== 1 || input.actions[0].kind !== 'reload')) return {
207
+ status: 'error', content: { code: 'browser_session_lost', required_actions: [{ type: 'reload' }] }, browserSessionRef: structuredClone(prior) };
208
+ let page = pages.get(request.runId), first = !page || page.server.entry !== entry || !!prior;
209
+ let navigationReason = prior ? 'session_restarted' : !page ? 'initial_open'
210
+ : page.server.entry !== entry ? 'entry_changed' : 'none';
211
+ if (page && first) { await closePage(request.runId, { closeBrowser: !prior }); checkOwned(); page = null; }
212
+ if (!page) {
213
+ try { page = await openPage(request.runId, input, request.operationKey, signal, prior); }
214
+ catch { signal.throwIfAborted(); return { status: 'error', content: { code: 'browser_start_failed' },
215
+ ...(prior ? { browserSessionRef: structuredClone(prior) } : {}) }; }
216
+ checkOwned(); lost.delete(request.runId);
217
+ }
218
+ let sourceRevision;
219
+ try { sourceRevision = page.server.revision(); }
220
+ catch (error) { return { status: 'error', content: { code: safeInput.has(error.code) ? error.code : 'browser_served_revision_unavailable' }, browserSessionRef: structuredClone(page.ref) }; }
221
+ const changed = sourceRevision !== page.sourceRevision;
222
+ if (changed && !first) navigationReason = 'source_changed';
223
+ targets.set(request.runId, { path: target, evidence: false, signal, assertCurrent: checkOwned });
224
+ const observations = [], actionResults = [], actions = prior ? [] : input.actions;
225
+ let dispatched = false;
226
+ try {
227
+ dispatched = true;
228
+ const initial = await act(page, request.operationKey, 0, first || changed ? 'navigate' : 'screenshot', signal);
229
+ checkOwned();
230
+ observations.push(initial);
231
+ if (initial.status !== 'ok' || !initial.complete) return result(observations, page, target, actionResults, navigationReason, request);
232
+ page.sourceRevision = sourceRevision;
233
+ if (input.waitMs) { await sleep(input.waitMs, signal); checkOwned(); }
234
+ for (const [index, action] of actions.entries()) {
235
+ checkOwned();
236
+ if (action.kind === 'wait') { await sleep(action.ms, signal); checkOwned(); actionResults.push({ index, type: 'wait', ok: true }); continue; }
237
+ const { kind, actionId: _, timeoutMs: __, ...fields } = action;
238
+ const seen = await act(page, request.operationKey, index + 1, kind === 'reload' ? 'navigate' : kind, signal,
239
+ Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== null)));
240
+ checkOwned();
241
+ observations.push(seen); actionResults.push({ index, type: kind, ok: seen.status === 'ok' });
242
+ if (seen.status !== 'ok' || !seen.complete) return result(observations, page, target, actionResults, navigationReason, request);
243
+ }
244
+ observations.push(await act(page, request.operationKey, actions.length + 1, 'screenshot', signal));
245
+ checkOwned();
246
+ return result(observations, page, target, actionResults, navigationReason, request);
247
+ } catch (error) {
248
+ const browserIsolation = dispatched ? await isolateUnknownPage(page, { ...request, signal }) : undefined;
249
+ const content = { code: 'browser_action_outcome_unknown', ...(browserIsolation ? { resource_continuity: isolationObservation() } : {}) };
250
+ return { status: dispatched ? 'uncertain' : 'error', content, browserSessionRef: structuredClone(page.ref),
251
+ ...(browserIsolation ? { browserIsolation, context: { content: canonical(content) } } : {}) };
252
+ }
253
+ });
254
+ },
255
+ /** Host-only capability retirement. This local resource serves only static
256
+ * GET/HEAD files, and its sink checks the live owner namespace. Removing the
257
+ * namespace fences late network/artifact work even after a lost context.
258
+ * It cannot assert that the original browser action succeeded. */
259
+ async retireResources({ scope: requestedScope, sessionId, runId, refs, signal: rawSignal }) {
260
+ const signal = combine(rawSignal); signal.throwIfAborted();
261
+ if (!same(requestedScope, scope) || typeof sessionId !== 'string' || !sessionId.trim()
262
+ || !Array.isArray(refs) || !refs.length || refs.length > 16) fail('browser_retirement_invalid');
263
+ const checked = refs.map(validateBrowserSessionRef);
264
+ if (checked.some(ref => !owns(ref) || ref.runId !== runId)) fail('browser_reference_scope_mismatch');
265
+ return serial(runId, signal, async () => {
266
+ retired.add(runId);
267
+ const page = pages.get(runId);
268
+ if (page) await closePage(runId, { signal, confirmClosed: true });
269
+ else for (const ref of checked) {
270
+ const report = validateBrowserRecovery(await browser.recover(ref, { signal }));
271
+ if (!same(report.sessionRef, ref) || report.status === 'restarted') fail('browser_recovery_reference_mismatch');
272
+ if (report.status === 'reconnected') await browser.close(ref, { signal, confirmClosed: true });
273
+ }
274
+ signal.throwIfAborted(); lost.delete(runId); targets.delete(runId);
275
+ return { contract: 'agentkit.resource-fence/v1', resourceKind: 'ephemeral-browser',
276
+ scope: structuredClone(scope), runId, sessionId, refs: structuredClone(checked),
277
+ fenced: true, externalEffects: false, oldActionsReplayed: false };
278
+ });
279
+ },
280
+ async recoverSessions({ refs, runId, signal: rawSignal }) {
281
+ const signal = combine(rawSignal); signal.throwIfAborted();
282
+ if (!Array.isArray(refs)) fail('invalid_browser_recovery');
283
+ const checked = [];
284
+ for (let index = 0; index < refs.length; index++) {
285
+ if (!Object.hasOwn(refs, index)) fail('invalid_browser_recovery');
286
+ const ref = validateBrowserSessionRef(refs[index]);
287
+ if (!owns(ref) || runId !== undefined && ref.runId !== runId) fail('browser_reference_scope_mismatch'); checked.push(ref);
288
+ }
289
+ return serialMany(checked.map(ref => ref.runId), signal, async () => {
290
+ const reports = [];
291
+ for (const ref of checked) {
292
+ if (retired.has(ref.runId)) { reports.push({ status: 'lost', sessionRef: ref,
293
+ lastActionSeq: 0, reason: 'resource_retired', reobserveAvailable: false }); continue; }
294
+ let report = validateBrowserRecovery(await browser.recover(ref, { signal }));
295
+ if (!same(report.sessionRef, ref) || report.status === 'restarted') fail('browser_recovery_reference_mismatch');
296
+ const page = pages.get(ref.runId);
297
+ if (report.status === 'reconnected' && (!page || page.revoked || !same(page.ref, ref) || !page.server.allows(page.server.url))) report = {
298
+ status: 'lost', sessionRef: ref, lastActionSeq: report.lastActionSeq,
299
+ reason: page?.revoked ? 'browser_action_outcome_unknown' : 'page_binding_unavailable', reobserveAvailable: false };
300
+ if (report.status === 'lost') report = { ...report, reobserveAvailable: reobserveAfterLoss };
301
+ reports.push(report);
302
+ }
303
+ signal.throwIfAborted();
304
+ for (const report of reports) if (report.status === 'lost') lost.set(report.sessionRef.runId, structuredClone(report.sessionRef));
305
+ return reports;
306
+ });
307
+ },
308
+ /** Trusted VerificationPort only. Fresh context and external image are observations,
309
+ * never a verdict that the complete user objective or visual quality passed. */
310
+ async verifyPage({ runId, entryPath, candidateId, signal: rawSignal, executeAction }) {
311
+ if (!evidence) fail('browser_verification_evidence_unavailable');
312
+ if (typeof candidateId !== 'string' || !candidateId.trim()) fail('browser_candidate_identity_invalid');
313
+ if (executeAction !== undefined && typeof executeAction !== 'function') fail('browser_verification_authority_invalid');
314
+ const input = parseWorkspaceBrowserInput({ path: entryPath, wait_ms: 0 }, `verify:${candidateId}`);
315
+ resolveBrowserWorkspaceEntry(workspace, input.path);
316
+ const signal = combine(rawSignal);
317
+ return serial(runId, signal, async () => {
318
+ if (retired.has(runId)) fail('browser_run_retired');
319
+ const verificationRunId = `${runId}:host-verify:${hash(candidateId).slice(0, 16)}:${++verificationAttempt}`;
320
+ const operationKey = `verify:${candidateId}:${verificationAttempt}`;
321
+ let page;
322
+ try {
323
+ page = await openPage(verificationRunId, input, operationKey, signal);
324
+ targets.set(verificationRunId, { path: join(evidence, `${hash(`${runId}\0${candidateId}\0${verificationAttempt}`)}.png`), evidence: true, signal });
325
+ const verifyAct = (index, kind, args = {}) => act(page, operationKey, index, kind, signal, args, executeAction);
326
+ const first = await verifyAct(0, 'navigate');
327
+ if (first.status !== 'ok') return first;
328
+ await sleep(1000, signal);
329
+ const settled = await verifyAct(1, 'screenshot'), observations = [first, settled];
330
+ if (settled.controls.some(control => control.clickable && control.duplicateId && launchLabel(control.label))) return {
331
+ ...settled, status: 'error', errorCode: 'browser_launch_control_ambiguous' };
332
+ const launch = settled.controls.filter(control => control.clickable && control.selector && launchLabel(control.label));
333
+ if (settled.status === 'ok' && launch.length === 1) {
334
+ const launched = await verifyAct(2, 'click', { selector: launch[0].selector }); observations.push(launched);
335
+ if (launched.status === 'ok') { await sleep(1000, signal); observations.push(await verifyAct(3, 'screenshot')); }
336
+ }
337
+ return { ...observations.at(-1), ...Object.fromEntries(['console', 'pageErrors', 'pageErrorLocations', 'networkFailures']
338
+ .map(key => [key, observations.flatMap(row => row[key])])) };
339
+ } finally { if (page) await closePage(verificationRunId); }
340
+ });
341
+ },
342
+ close() {
343
+ if (closing) return closing;
344
+ closed = true; lifetime.abort(new Error('browser_owner_closed'));
345
+ closing = (async () => {
346
+ await Promise.allSettled([...locks.values()]);
347
+ const results = await Promise.allSettled([...pages.keys()].map(runId => closePage(runId)));
348
+ try { if (typeof browser.dispose === 'function') await browser.dispose(); else await browser.aclose?.(); }
349
+ finally { lost.clear(); targets.clear(); }
350
+ const failed = results.find(result => result.status === 'rejected'); if (failed) throw failed.reason;
351
+ })();
352
+ return closing;
353
+ },
354
+ };
355
+ }
@@ -0,0 +1,3 @@
1
+ // One pure TypeScript codec is shared by Node ports and the runtime.
2
+ export { CAPTURED_SOURCE_MEDIA_TYPE, CAPTURED_SOURCE_VERSION, encodeCapturedSource, decodeCapturedSource }
3
+ from '@kingsoftcloud/agentkit-harness-core/captured-source';
@@ -0,0 +1,87 @@
1
+ import { NativeError } from '@kingsoftcloud/agentkit-harness-core';
2
+
3
+ /**
4
+ * Opt-in child allocation policy. When a parent Run admits children under this
5
+ * policy, omitted per-role and per-call limits defer to a bounded useful allowance
6
+ * capped by the parent's actual remaining budget, instead of stacking two unrelated
7
+ * 4-call defaults. Absent policy preserves the exact legacy limits, digests and cold
8
+ * recovery.
9
+ */
10
+ export const CHILD_ALLOCATION_POLICY = 'parent-shared/v1';
11
+
12
+ /** Bounded useful child allowance used when a role omits the limit under the policy. */
13
+ export const CHILD_ALLOCATION_DEFAULTS = Object.freeze({ modelCalls: 16, toolCalls: 64, timeoutSeconds: 900 });
14
+
15
+ /** Legacy call-level defaults applied when no allocation policy is active. */
16
+ export const LEGACY_CALL_DEFAULTS = Object.freeze({ modelCallLimit: 4, toolCallLimit: 8 });
17
+
18
+ const isPolicy = value => value === undefined || value === CHILD_ALLOCATION_POLICY;
19
+
20
+ /**
21
+ * Role-level defaults for `nativeChildRoles`. Under the policy an omitted `maxTurns`
22
+ * becomes the 16-call allowance (so the existing `maxTurns * 4` tool formula yields 64),
23
+ * `timeoutSeconds` becomes 900s, and `maxToolCalls` stays `null` (the resolveChild
24
+ * formula fills it). Legacy returns the historical 4 / null / 120 so old normalized
25
+ * role shapes and digests are byte-identical. Unknown policies are rejected.
26
+ */
27
+ export function roleAllocationDefaults(policy) {
28
+ if (!isPolicy(policy)) throw new NativeError('studio_native_child_allocation_policy_invalid');
29
+ if (policy === CHILD_ALLOCATION_POLICY) {
30
+ return { maxTurns: CHILD_ALLOCATION_DEFAULTS.modelCalls, maxToolCalls: null,
31
+ timeoutSeconds: CHILD_ALLOCATION_DEFAULTS.timeoutSeconds };
32
+ }
33
+ return { maxTurns: 4, maxToolCalls: null, timeoutSeconds: 120 };
34
+ }
35
+
36
+ /**
37
+ * Resolve the call-level limits a parent tool invocation requests for a child, after
38
+ * the call-level clamp but before the role-cap clamp (which `createNativeChildOwner`
39
+ * applies via `binding.maxModelCalls = min(maxModelCalls, roleCap)`).
40
+ *
41
+ * Under the policy, omitted `modelCallLimit`/`toolCallLimit` defer (`null`) to the role
42
+ * cap + remaining parent budget — the stacked 4/8 default is not applied. Under legacy,
43
+ * omitted limits fall back to 4/8. Explicit user limits are always honored and
44
+ * validated. Unknown policies are rejected.
45
+ *
46
+ * `availableTools` is intentionally kept signed (NOT clamped to 0): a `start/v1` call
47
+ * with `maxToolCallsRemaining === 0` must be rejected before any child starts, so the
48
+ * owner refuses admission rather than core rejecting the receipt after execution. The
49
+ * caller checks `maxModelCalls > 0 && maxToolCalls >= 0` and the helper never produces an
50
+ * `Infinity` that would cross a copied/serialized request or binding: when a remaining
51
+ * budget is `null` (unlimited) and a limit is deferred, the allowance's finite cap is
52
+ * used instead, so a standalone resolver with `null` role caps still admits a bounded
53
+ * child rather than an unbounded one.
54
+ */
55
+ export function resolveChildCallLimits({ policy, modelCallLimit, toolCallLimit, budget, childBudgetPolicy }) {
56
+ if (!isPolicy(policy)) throw new NativeError('studio_native_child_allocation_policy_invalid');
57
+ if (budget == null || typeof budget !== 'object') throw new NativeError('child_budget_invalid');
58
+ const defer = policy === CHILD_ALLOCATION_POLICY;
59
+ const modelLimit = defer ? (modelCallLimit ?? null) : (modelCallLimit ?? LEGACY_CALL_DEFAULTS.modelCallLimit);
60
+ const toolLimit = defer ? (toolCallLimit ?? null) : (toolCallLimit ?? LEGACY_CALL_DEFAULTS.toolCallLimit);
61
+ if (!((modelLimit === null || Number.isSafeInteger(modelLimit) && modelLimit >= 1)
62
+ && (toolLimit === null || Number.isSafeInteger(toolLimit) && toolLimit >= 0))) {
63
+ throw new NativeError('child_budget_invalid');
64
+ }
65
+ // start/v1 consumes one of the parent's remaining tool calls for the admission itself;
66
+ // delegate/v1 does not. Keep the result signed so an exhausted parent is refused here.
67
+ const reserve = childBudgetPolicy === 'start/v1' ? 1 : 0;
68
+ const availableTools = budget.maxToolCallsRemaining === null ? null : budget.maxToolCallsRemaining - reserve;
69
+ // Under the policy the allowance is the default ceiling: a deferred limit is capped by
70
+ // the allowance, and the parent's remaining budget caps it further (min). An unlimited
71
+ // parent leaves the allowance as the finite result. Under legacy, omitted limits fall
72
+ // back to 4/8; an unlimited parent keeps 4/8, a bounded parent caps them. Explicit
73
+ // limits always win and are capped by a bounded parent. No Infinity ever reaches a
74
+ // copied or serialized request or binding: deferred-against-unlimited yields the finite
75
+ // allowance, and every other branch is bounded by an explicit limit, the legacy 4/8,
76
+ // or a bounded parent.
77
+ const modelCeiling = defer ? CHILD_ALLOCATION_DEFAULTS.modelCalls : LEGACY_CALL_DEFAULTS.modelCallLimit;
78
+ const toolCeiling = defer ? CHILD_ALLOCATION_DEFAULTS.toolCalls : LEGACY_CALL_DEFAULTS.toolCallLimit;
79
+ const unlimited = { model: budget.maxModelCallsRemaining === null, tool: availableTools === null };
80
+ const maxModelCalls = modelLimit === null
81
+ ? (unlimited.model ? modelCeiling : Math.min(modelCeiling, budget.maxModelCallsRemaining))
82
+ : (unlimited.model ? modelLimit : Math.min(modelLimit, budget.maxModelCallsRemaining));
83
+ const maxToolCalls = toolLimit === null
84
+ ? (unlimited.tool ? toolCeiling : Math.min(toolCeiling, availableTools))
85
+ : (unlimited.tool ? toolLimit : Math.min(toolLimit, availableTools));
86
+ return { maxModelCalls, maxToolCalls };
87
+ }
@@ -0,0 +1,35 @@
1
+ import { invariant } from '@kingsoftcloud/agentkit-harness-core';
2
+
3
+ export const CHILD_CAPABILITY_POLICY = 'required-tools/v1';
4
+ export const childRequiredToolsSchema = () => ({ type: 'array', maxItems: 128, uniqueItems: true,
5
+ items: { type: 'string', minLength: 1, maxLength: 256 },
6
+ description: 'Exact tool names needed by this task. Use only capabilities in the selected role directory; [] means no tool requirement. Missing capabilities refuse admission without starting a child.' });
7
+
8
+ const toolName = value => typeof value === 'string' && value.length > 0 && value.length <= 256
9
+ && !/[\s\x00-\x1f\x7f]/u.test(value);
10
+ const names = values => [...new Set(values)].sort();
11
+
12
+ /** Derive the directory from already approved definitions and explicit inheritance.
13
+ * This is a view of grants, never a grant resolver or task-language classifier. */
14
+ export function childCapabilityDirectory(roles, { tools, mcpToolNames = [], skillToolNames = [] }) {
15
+ const available = new Set(tools.map(tool => typeof tool === 'string' ? tool : tool.name));
16
+ invariant(roles.length <= 32 && roles.every(role => typeof role.name === 'string' && Array.isArray(role.tools)),
17
+ 'invalid_child_capability_directory');
18
+ return roles.map(role => ({ role: role.name, tools: names([...role.tools,
19
+ ...(role.inheritMcp ? mcpToolNames : []), ...(role.inheritSkills ? skillToolNames : [])])
20
+ .filter(name => available.has(name)) })).sort((a, b) => a.role.localeCompare(b.role, 'en'));
21
+ }
22
+
23
+ /** Known no-effect refusal. Call only after the parent intent/fence is validated. */
24
+ export function admitChildCapabilities(directory, role, requiredTools) {
25
+ if (!Array.isArray(requiredTools) || requiredTools.length > 128 || !requiredTools.every(toolName)
26
+ || new Set(requiredTools).size !== requiredTools.length)
27
+ return { code: 'child_capability_request_invalid', executed: false };
28
+ const selected = directory.find(item => item.role === role);
29
+ if (!selected) return { code: 'child_role_unavailable', executed: false, available_roles: directory.map(item => item.role) };
30
+ const required = names(requiredTools), missing = required.filter(name => !selected.tools.includes(name));
31
+ if (!missing.length) return null;
32
+ return { code: 'child_capability_unavailable', executed: false, role: selected.role,
33
+ missing_tools: missing, available_tools: [...selected.tools], suitable_roles: directory
34
+ .filter(item => required.every(name => item.tools.includes(name))).map(item => item.role) };
35
+ }
@@ -0,0 +1,20 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { canonical, invariant } from '@kingsoftcloud/agentkit-harness-core';
3
+
4
+ export const CHILD_COMMAND_RESOURCE = 'agentkit.child-local-command-resource/v1';
5
+
6
+ /** Host capability identity only. Environment values stay with the owner; the
7
+ * descriptor cannot grant a tool, choose a model path or bypass shell policy. */
8
+ export function childCommandResource(commandBinding) {
9
+ if (!commandBinding || commandBinding.backend !== 'local_process') return null;
10
+ invariant(commandBinding.contract === 'agentkit.local-command-tools/v1'
11
+ && commandBinding.controls?.contract === 'agentkit.local-process-control/v1'
12
+ && /^[a-f0-9]{64}$/.test(commandBinding.environmentDigest), 'native_child_command_binding_invalid');
13
+ return Object.freeze({ contract: CHILD_COMMAND_RESOURCE, backend: 'local_process',
14
+ bindingDigest: createHash('sha256').update(canonical(commandBinding)).digest('hex') });
15
+ }
16
+
17
+ export function validChildCommandResource(value) {
18
+ return value?.contract === CHILD_COMMAND_RESOURCE && value.backend === 'local_process'
19
+ && /^[a-f0-9]{64}$/.test(value.bindingDigest) && Object.keys(value).length === 3;
20
+ }