@yuzc-001/grasp 0.6.6
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/LICENSE +21 -0
- package/README.md +327 -0
- package/README.zh-CN.md +324 -0
- package/examples/README.md +31 -0
- package/examples/claude-desktop.json +8 -0
- package/examples/codex-config.toml +4 -0
- package/grasp.skill +0 -0
- package/index.js +87 -0
- package/package.json +48 -0
- package/scripts/grasp_openclaw_ctl.sh +122 -0
- package/scripts/run-search-benchmark.mjs +287 -0
- package/scripts/update-star-history.mjs +274 -0
- package/skill/SKILL.md +61 -0
- package/skill/references/tools.md +306 -0
- package/src/cli/auto-configure.js +116 -0
- package/src/cli/cmd-connect.js +148 -0
- package/src/cli/cmd-explain.js +42 -0
- package/src/cli/cmd-logs.js +55 -0
- package/src/cli/cmd-status.js +119 -0
- package/src/cli/config.js +27 -0
- package/src/cli/detect-chrome.js +58 -0
- package/src/grasp/handoff/events.js +67 -0
- package/src/grasp/handoff/persist.js +48 -0
- package/src/grasp/handoff/state.js +28 -0
- package/src/grasp/page/capture.js +34 -0
- package/src/grasp/page/state.js +273 -0
- package/src/grasp/verify/evidence.js +40 -0
- package/src/grasp/verify/pipeline.js +52 -0
- package/src/layer1-bridge/chrome.js +416 -0
- package/src/layer1-bridge/webmcp.js +143 -0
- package/src/layer2-perception/hints.js +284 -0
- package/src/layer3-action/actions.js +400 -0
- package/src/runtime/browser-instance.js +65 -0
- package/src/runtime/truth/model.js +94 -0
- package/src/runtime/truth/snapshot.js +51 -0
- package/src/server/affordances.js +47 -0
- package/src/server/audit.js +122 -0
- package/src/server/boss-fast-path.js +164 -0
- package/src/server/boundary-guard.js +53 -0
- package/src/server/content.js +97 -0
- package/src/server/continuity.js +256 -0
- package/src/server/engine-selection.js +29 -0
- package/src/server/entry-orchestrator.js +115 -0
- package/src/server/error-codes.js +7 -0
- package/src/server/explain-share-card.js +113 -0
- package/src/server/fast-path-router.js +134 -0
- package/src/server/form-runtime.js +602 -0
- package/src/server/form-tasks.js +254 -0
- package/src/server/gateway-response.js +62 -0
- package/src/server/index.js +22 -0
- package/src/server/observe.js +52 -0
- package/src/server/page-projection.js +31 -0
- package/src/server/page-state.js +27 -0
- package/src/server/postconditions.js +128 -0
- package/src/server/prompt-assembly.js +148 -0
- package/src/server/responses.js +44 -0
- package/src/server/route-boundary.js +174 -0
- package/src/server/route-policy.js +168 -0
- package/src/server/runtime-confirmation.js +87 -0
- package/src/server/runtime-status.js +7 -0
- package/src/server/share-artifacts.js +284 -0
- package/src/server/state.js +132 -0
- package/src/server/structured-extraction.js +131 -0
- package/src/server/surface-prompts.js +166 -0
- package/src/server/task-frame.js +11 -0
- package/src/server/tasks/search-task.js +321 -0
- package/src/server/tools.actions.js +1361 -0
- package/src/server/tools.form.js +526 -0
- package/src/server/tools.gateway.js +757 -0
- package/src/server/tools.handoff.js +210 -0
- package/src/server/tools.js +20 -0
- package/src/server/tools.legacy.js +983 -0
- package/src/server/tools.strategy.js +250 -0
- package/src/server/tools.task-surface.js +66 -0
- package/src/server/tools.workspace.js +873 -0
- package/src/server/workspace-runtime.js +1138 -0
- package/src/server/workspace-tasks.js +735 -0
- package/start-chrome.bat +84 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { buildAgentBoundary } from './route-boundary.js';
|
|
2
|
+
import { getBoundaryPromptPack, getSurfacePromptPack } from './surface-prompts.js';
|
|
3
|
+
|
|
4
|
+
function getPageRole(page = {}) {
|
|
5
|
+
return page?.page_role ?? page?.current_role ?? page?.currentRole ?? null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function formatList(items = []) {
|
|
9
|
+
return Array.isArray(items) && items.length > 0
|
|
10
|
+
? items.map((item) => `- ${item}`).join('\n')
|
|
11
|
+
: '- none';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readWorkspaceSurface(result = {}, route = {}, page = {}) {
|
|
15
|
+
return result?.workspace?.workspace_surface
|
|
16
|
+
?? result?.snapshot?.workspace_surface
|
|
17
|
+
?? route?.evidence?.workspace_surface
|
|
18
|
+
?? page?.workspace_surface
|
|
19
|
+
?? page?.workspaceSurface
|
|
20
|
+
?? null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function inferPromptSurfaceKey({ boundaryKey, status, result = {}, route = {}, page = {} } = {}) {
|
|
24
|
+
if (!boundaryKey) return null;
|
|
25
|
+
|
|
26
|
+
if (boundaryKey === 'public_read') {
|
|
27
|
+
return getPageRole(page) === 'search' ? 'public_search' : 'public_content';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (boundaryKey === 'live_session') {
|
|
31
|
+
return getPageRole(page) === 'auth' ? 'live_auth_session' : 'live_runtime_surface';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (boundaryKey === 'session_warmup') {
|
|
35
|
+
return 'session_reentry';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (boundaryKey === 'form_runtime') {
|
|
39
|
+
const completionStatus = result?.form?.completion_status ?? null;
|
|
40
|
+
if (completionStatus === 'review_required') return 'form_review_required';
|
|
41
|
+
if (completionStatus === 'ready_to_submit' || completionStatus === 'complete') {
|
|
42
|
+
return 'form_ready_to_submit';
|
|
43
|
+
}
|
|
44
|
+
return 'form_surface';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (boundaryKey === 'workspace_runtime') {
|
|
48
|
+
const workspaceSurface = readWorkspaceSurface(result, route, page);
|
|
49
|
+
if (workspaceSurface === 'list') return 'workspace_list';
|
|
50
|
+
if (workspaceSurface === 'detail') return 'workspace_detail';
|
|
51
|
+
if (workspaceSurface === 'thread') return 'workspace_thread';
|
|
52
|
+
if (workspaceSurface === 'composer') return 'workspace_composer';
|
|
53
|
+
if (workspaceSurface === 'loading_shell') return 'workspace_loading_shell';
|
|
54
|
+
return 'workspace_surface';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (boundaryKey === 'handoff') {
|
|
58
|
+
return 'checkpoint_handoff';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (status === 'gated') {
|
|
62
|
+
return 'checkpoint_handoff';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildSegment(id, lines) {
|
|
69
|
+
return {
|
|
70
|
+
id,
|
|
71
|
+
text: lines.filter(Boolean).join('\n'),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildAgentPrompt({
|
|
76
|
+
status,
|
|
77
|
+
page,
|
|
78
|
+
result = {},
|
|
79
|
+
continuation = {},
|
|
80
|
+
route = null,
|
|
81
|
+
agentBoundary = null,
|
|
82
|
+
} = {}) {
|
|
83
|
+
const boundary = agentBoundary ?? buildAgentBoundary({
|
|
84
|
+
status,
|
|
85
|
+
page,
|
|
86
|
+
result,
|
|
87
|
+
continuation,
|
|
88
|
+
route,
|
|
89
|
+
});
|
|
90
|
+
if (!boundary) return null;
|
|
91
|
+
|
|
92
|
+
const boundaryPack = getBoundaryPromptPack(boundary.key);
|
|
93
|
+
const surfaceKey = inferPromptSurfaceKey({
|
|
94
|
+
boundaryKey: boundary.key,
|
|
95
|
+
status,
|
|
96
|
+
result,
|
|
97
|
+
route,
|
|
98
|
+
page,
|
|
99
|
+
});
|
|
100
|
+
const surfacePack = getSurfacePromptPack(surfaceKey);
|
|
101
|
+
const nextStep = boundary.next_step ?? continuation?.suggested_next_action ?? null;
|
|
102
|
+
|
|
103
|
+
const segments = [
|
|
104
|
+
buildSegment('runtime_identity', [
|
|
105
|
+
'You are operating inside Grasp, a route-aware browser runtime.',
|
|
106
|
+
'Respect the current runtime boundary, keep actions explainable, and switch modes only when runtime evidence changes.',
|
|
107
|
+
]),
|
|
108
|
+
buildSegment(`boundary_${boundary.key}`, [
|
|
109
|
+
`Current boundary: ${boundary.key}`,
|
|
110
|
+
`Boundary summary: ${boundary.summary}`,
|
|
111
|
+
'Preferred tools:',
|
|
112
|
+
formatList(boundary.preferred_tools),
|
|
113
|
+
'Avoid:',
|
|
114
|
+
formatList(boundary.avoid),
|
|
115
|
+
boundary.confirmation ? `Explicit confirmation gate: ${boundary.confirmation}` : null,
|
|
116
|
+
]),
|
|
117
|
+
boundaryPack
|
|
118
|
+
? buildSegment(`boundary_pack_${boundaryPack.id}`, [
|
|
119
|
+
`Boundary pack: ${boundaryPack.id}`,
|
|
120
|
+
...boundaryPack.instructions.map((line) => `- ${line}`),
|
|
121
|
+
])
|
|
122
|
+
: null,
|
|
123
|
+
surfacePack
|
|
124
|
+
? buildSegment(`surface_pack_${surfacePack.id}`, [
|
|
125
|
+
`Surface pack: ${surfacePack.id}`,
|
|
126
|
+
...surfacePack.instructions.map((line) => `- ${line}`),
|
|
127
|
+
])
|
|
128
|
+
: null,
|
|
129
|
+
buildSegment('next_step', [
|
|
130
|
+
`Next best step: ${nextStep ?? 'unknown'}`,
|
|
131
|
+
]),
|
|
132
|
+
].filter(Boolean);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
boundary_key: boundary.key,
|
|
136
|
+
surface_key: surfaceKey,
|
|
137
|
+
prompt_pack: {
|
|
138
|
+
boundary: boundaryPack?.id ?? boundary.key,
|
|
139
|
+
surface: surfacePack?.id ?? surfaceKey ?? null,
|
|
140
|
+
},
|
|
141
|
+
preferred_tools: [...boundary.preferred_tools],
|
|
142
|
+
avoid: [...boundary.avoid],
|
|
143
|
+
confirmation: boundary.confirmation,
|
|
144
|
+
next_step: nextStep,
|
|
145
|
+
segments,
|
|
146
|
+
system_prompt: segments.map((segment) => segment.text).join('\n\n'),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function normalizeText(value) {
|
|
2
|
+
if (Array.isArray(value)) {
|
|
3
|
+
return value.filter((line) => line !== undefined && line !== null && line !== '').join('\n');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
return String(value ?? '');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function textResponse(value, meta) {
|
|
10
|
+
const response = {
|
|
11
|
+
content: [
|
|
12
|
+
{
|
|
13
|
+
type: 'text',
|
|
14
|
+
text: normalizeText(value),
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
if (meta && Object.keys(meta).length > 0) {
|
|
20
|
+
response.meta = meta;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return response;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function errorResponse(value, meta = {}) {
|
|
27
|
+
return {
|
|
28
|
+
...textResponse(value, meta),
|
|
29
|
+
isError: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function imageResponse(data, mimeType = 'image/png') {
|
|
34
|
+
const base64 = Buffer.isBuffer(data) ? data.toString('base64') : String(data);
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: 'image',
|
|
39
|
+
data: base64,
|
|
40
|
+
mimeType,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
const BOUNDARY_TEMPLATES = {
|
|
2
|
+
public_read: {
|
|
3
|
+
summary: 'Stay on the read/extract path. Prefer inspect, extract, extract_structured, extract_batch, share_page, and continue before low-level actions.',
|
|
4
|
+
preferred_tools: ['inspect', 'extract', 'extract_structured', 'extract_batch', 'share_page', 'continue'],
|
|
5
|
+
avoid: ['page-changing actions', 'form_runtime tools', 'workspace_runtime tools'],
|
|
6
|
+
confirmation: null,
|
|
7
|
+
next_step: 'inspect',
|
|
8
|
+
},
|
|
9
|
+
live_session: {
|
|
10
|
+
summary: 'Stay on the live session runtime loop. Prefer inspect, extract, continue, and explain_route until a specialized surface is clearly present.',
|
|
11
|
+
preferred_tools: ['inspect', 'extract', 'continue', 'explain_route'],
|
|
12
|
+
avoid: ['premature low-level primitives', 'form_runtime tools without a form surface', 'workspace_runtime tools without a workspace surface'],
|
|
13
|
+
confirmation: null,
|
|
14
|
+
next_step: 'inspect',
|
|
15
|
+
},
|
|
16
|
+
session_warmup: {
|
|
17
|
+
summary: 'Warm the session before direct entry. Use preheat_session first, then re-enter the runtime loop after trust improves.',
|
|
18
|
+
preferred_tools: ['preheat_session', 'entry', 'inspect'],
|
|
19
|
+
avoid: ['repeated direct retries', 'form_runtime tools', 'workspace_runtime tools'],
|
|
20
|
+
confirmation: null,
|
|
21
|
+
next_step: 'preheat_session',
|
|
22
|
+
},
|
|
23
|
+
form_runtime: {
|
|
24
|
+
summary: 'Use the form surface. Fill safe text fields first, keep review-tier controls explicit, and submit only through safe_submit.',
|
|
25
|
+
preferred_tools: ['form_inspect', 'fill_form', 'set_option', 'set_date', 'verify_form', 'safe_submit'],
|
|
26
|
+
avoid: ['blind field writes', 'raw submit clicks', 'navigate away mid-form'],
|
|
27
|
+
confirmation: 'safe_submit(mode="confirm", confirmation="SUBMIT")',
|
|
28
|
+
next_step: 'verify_form',
|
|
29
|
+
},
|
|
30
|
+
workspace_runtime: {
|
|
31
|
+
summary: 'Use the workspace surface. Select the live item, draft safely, preview sends, and execute only through execute_action.',
|
|
32
|
+
preferred_tools: ['workspace_inspect', 'select_live_item', 'draft_action', 'execute_action', 'verify_outcome'],
|
|
33
|
+
avoid: ['raw send clicks', 'press Enter to send', 'execute before draft or preview'],
|
|
34
|
+
confirmation: 'execute_action(mode="confirm", confirmation="EXECUTE")',
|
|
35
|
+
next_step: 'workspace_inspect',
|
|
36
|
+
},
|
|
37
|
+
handoff: {
|
|
38
|
+
summary: 'This flow is blocked on handoff. Stop direct action attempts, persist the handoff step, let the human recover the page, then resume.',
|
|
39
|
+
preferred_tools: ['request_handoff', 'mark_handoff_in_progress', 'mark_handoff_done', 'resume_after_handoff', 'continue'],
|
|
40
|
+
avoid: ['looping retries', 'form_runtime actions while gated', 'workspace_runtime actions while gated'],
|
|
41
|
+
confirmation: null,
|
|
42
|
+
next_step: 'request_handoff',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function getPageRole(page = null) {
|
|
47
|
+
return page?.page_role ?? page?.current_role ?? page?.currentRole ?? null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function inferSurfaceBoundaryKey({
|
|
51
|
+
result = {},
|
|
52
|
+
route = null,
|
|
53
|
+
page = null,
|
|
54
|
+
} = {}) {
|
|
55
|
+
if (result?.task_kind === 'form' || route?.selected_mode === 'form_runtime') {
|
|
56
|
+
return 'form_runtime';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (result?.task_kind === 'workspace' || route?.selected_mode === 'workspace_runtime') {
|
|
60
|
+
return 'workspace_runtime';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (route?.selected_mode === 'public_read') {
|
|
64
|
+
return 'public_read';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (route?.selected_mode === 'live_session') {
|
|
68
|
+
return 'live_session';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const pageRole = getPageRole(page);
|
|
72
|
+
if (pageRole === 'form') {
|
|
73
|
+
return 'form_runtime';
|
|
74
|
+
}
|
|
75
|
+
if (pageRole === 'workspace') {
|
|
76
|
+
return 'workspace_runtime';
|
|
77
|
+
}
|
|
78
|
+
if (pageRole === 'auth') {
|
|
79
|
+
return 'live_session';
|
|
80
|
+
}
|
|
81
|
+
if (['content', 'docs', 'search', 'navigation-heavy'].includes(String(pageRole ?? ''))) {
|
|
82
|
+
return 'public_read';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function inferAgentBoundaryKey({
|
|
89
|
+
status,
|
|
90
|
+
result = {},
|
|
91
|
+
continuation = {},
|
|
92
|
+
route = null,
|
|
93
|
+
page = null,
|
|
94
|
+
} = {}) {
|
|
95
|
+
if (
|
|
96
|
+
status === 'handoff_required'
|
|
97
|
+
|| status === 'gated'
|
|
98
|
+
|| route?.selected_mode === 'handoff'
|
|
99
|
+
|| continuation?.suggested_next_action === 'request_handoff'
|
|
100
|
+
) {
|
|
101
|
+
return 'handoff';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (result?.task_kind === 'form' || route?.selected_mode === 'form_runtime') {
|
|
105
|
+
return 'form_runtime';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (result?.task_kind === 'workspace' || route?.selected_mode === 'workspace_runtime') {
|
|
109
|
+
return 'workspace_runtime';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (status === 'warmup' || continuation?.suggested_next_action === 'preheat_session') {
|
|
113
|
+
return 'session_warmup';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return inferSurfaceBoundaryKey({ result, route, page });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function getBoundaryDefaultNextStep(key) {
|
|
120
|
+
return BOUNDARY_TEMPLATES[key]?.next_step ?? null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function buildBoundaryContinuation(key, handoffState = 'idle') {
|
|
124
|
+
if (!key) {
|
|
125
|
+
return {
|
|
126
|
+
can_continue: false,
|
|
127
|
+
suggested_next_action: null,
|
|
128
|
+
handoff_state: handoffState,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
can_continue: key !== 'handoff',
|
|
134
|
+
suggested_next_action: getBoundaryDefaultNextStep(key),
|
|
135
|
+
handoff_state: handoffState,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function buildBoundaryMismatchLines({
|
|
140
|
+
toolName,
|
|
141
|
+
expectedBoundary,
|
|
142
|
+
currentBoundary,
|
|
143
|
+
nextStep = null,
|
|
144
|
+
} = {}) {
|
|
145
|
+
return [
|
|
146
|
+
`Boundary mismatch: ${toolName} requires ${expectedBoundary}, but the current surface is ${currentBoundary}.`,
|
|
147
|
+
nextStep ? `Recover by running ${nextStep} before trying ${toolName} again.` : null,
|
|
148
|
+
].filter(Boolean);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function buildAgentBoundary(input = {}) {
|
|
152
|
+
const key = inferAgentBoundaryKey(input);
|
|
153
|
+
if (!key) return null;
|
|
154
|
+
|
|
155
|
+
const template = BOUNDARY_TEMPLATES[key];
|
|
156
|
+
return {
|
|
157
|
+
key,
|
|
158
|
+
summary: template.summary,
|
|
159
|
+
preferred_tools: [...template.preferred_tools],
|
|
160
|
+
avoid: [...template.avoid],
|
|
161
|
+
confirmation: template.confirmation,
|
|
162
|
+
next_step: input?.continuation?.suggested_next_action ?? template.next_step ?? null,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function buildAgentBoundaryLines(boundary) {
|
|
167
|
+
if (!boundary) return [];
|
|
168
|
+
|
|
169
|
+
return [
|
|
170
|
+
`Boundary: ${boundary.key}`,
|
|
171
|
+
`Boundary guidance: ${boundary.summary}`,
|
|
172
|
+
boundary.confirmation ? `Boundary confirmation: ${boundary.confirmation}` : null,
|
|
173
|
+
].filter(Boolean);
|
|
174
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
function isBlockedHandoffState(handoffState) {
|
|
2
|
+
return handoffState === 'handoff_required'
|
|
3
|
+
|| handoffState === 'handoff_in_progress'
|
|
4
|
+
|| handoffState === 'awaiting_reacquisition';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function buildEvidence({
|
|
8
|
+
intent,
|
|
9
|
+
selection,
|
|
10
|
+
preflight,
|
|
11
|
+
pageState,
|
|
12
|
+
handoff,
|
|
13
|
+
triggers,
|
|
14
|
+
}) {
|
|
15
|
+
return {
|
|
16
|
+
intent,
|
|
17
|
+
engine: selection?.engine ?? 'runtime',
|
|
18
|
+
session_trust: preflight?.session_trust ?? 'unknown',
|
|
19
|
+
recommended_entry_strategy: preflight?.recommended_entry_strategy ?? 'direct',
|
|
20
|
+
page_role: pageState?.currentRole ?? 'unknown',
|
|
21
|
+
workspace_surface: pageState?.workspaceSurface ?? null,
|
|
22
|
+
handoff_state: handoff?.state ?? 'idle',
|
|
23
|
+
risk_gate_detected: pageState?.riskGateDetected ?? false,
|
|
24
|
+
triggers,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function inferIntentFromPageState(pageState = {}, lastIntent = null) {
|
|
29
|
+
if (lastIntent) return lastIntent;
|
|
30
|
+
|
|
31
|
+
const currentRole = pageState?.currentRole ?? 'unknown';
|
|
32
|
+
if (currentRole === 'workspace' || pageState?.workspaceSurface != null) {
|
|
33
|
+
return 'workspace';
|
|
34
|
+
}
|
|
35
|
+
if (currentRole === 'form' || currentRole === 'auth') {
|
|
36
|
+
return 'submit';
|
|
37
|
+
}
|
|
38
|
+
return 'extract';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolveRouteIntent({ intent = null, pageState = {}, lastIntent = null } = {}) {
|
|
42
|
+
return inferIntentFromPageState(pageState, intent ?? lastIntent);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function decideRoute({
|
|
46
|
+
url,
|
|
47
|
+
intent = 'extract',
|
|
48
|
+
selection = {},
|
|
49
|
+
preflight = {},
|
|
50
|
+
pageState = {},
|
|
51
|
+
handoff = {},
|
|
52
|
+
} = {}) {
|
|
53
|
+
const handoffState = handoff?.state ?? 'idle';
|
|
54
|
+
const currentRole = pageState?.currentRole ?? 'unknown';
|
|
55
|
+
const workspaceLike = currentRole === 'workspace' || pageState?.workspaceSurface != null;
|
|
56
|
+
const formLike = currentRole === 'form' || currentRole === 'auth';
|
|
57
|
+
|
|
58
|
+
if (isBlockedHandoffState(handoffState) || pageState?.riskGateDetected || currentRole === 'checkpoint') {
|
|
59
|
+
return {
|
|
60
|
+
policy_template: 'gated_handoff',
|
|
61
|
+
selected_mode: 'handoff',
|
|
62
|
+
confidence: 'high',
|
|
63
|
+
evidence: buildEvidence({
|
|
64
|
+
intent,
|
|
65
|
+
selection,
|
|
66
|
+
preflight,
|
|
67
|
+
pageState,
|
|
68
|
+
handoff,
|
|
69
|
+
triggers: ['gated_page_or_handoff_state'],
|
|
70
|
+
}),
|
|
71
|
+
alternatives: [],
|
|
72
|
+
fallback_chain: [],
|
|
73
|
+
requires_human: true,
|
|
74
|
+
risk_level: 'high',
|
|
75
|
+
next_step: 'request_handoff',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (intent === 'workspace' || workspaceLike) {
|
|
80
|
+
return {
|
|
81
|
+
policy_template: 'dynamic_workspace',
|
|
82
|
+
selected_mode: 'workspace_runtime',
|
|
83
|
+
confidence: intent === 'workspace' ? 'high' : 'medium',
|
|
84
|
+
evidence: buildEvidence({
|
|
85
|
+
intent,
|
|
86
|
+
selection,
|
|
87
|
+
preflight,
|
|
88
|
+
pageState,
|
|
89
|
+
handoff,
|
|
90
|
+
triggers: [intent === 'workspace' ? 'workspace_intent' : 'workspace_surface'],
|
|
91
|
+
}),
|
|
92
|
+
alternatives: [
|
|
93
|
+
{ mode: 'handoff', reason: 'human_required_if_workspace_progress_blocks' },
|
|
94
|
+
],
|
|
95
|
+
fallback_chain: ['handoff'],
|
|
96
|
+
requires_human: false,
|
|
97
|
+
risk_level: 'medium',
|
|
98
|
+
next_step: 'workspace_inspect',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (intent === 'submit' || formLike) {
|
|
103
|
+
return {
|
|
104
|
+
policy_template: 'real_form',
|
|
105
|
+
selected_mode: 'form_runtime',
|
|
106
|
+
confidence: intent === 'submit' ? 'high' : 'medium',
|
|
107
|
+
evidence: buildEvidence({
|
|
108
|
+
intent,
|
|
109
|
+
selection,
|
|
110
|
+
preflight,
|
|
111
|
+
pageState,
|
|
112
|
+
handoff,
|
|
113
|
+
triggers: [intent === 'submit' ? 'submit_intent' : 'form_surface'],
|
|
114
|
+
}),
|
|
115
|
+
alternatives: [
|
|
116
|
+
{ mode: 'handoff', reason: 'human_required_for_sensitive_or_blocked_form_steps' },
|
|
117
|
+
],
|
|
118
|
+
fallback_chain: ['handoff'],
|
|
119
|
+
requires_human: false,
|
|
120
|
+
risk_level: 'high',
|
|
121
|
+
next_step: 'form_inspect',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if ((intent === 'read' || intent === 'extract' || intent === 'collect') && selection?.engine === 'data') {
|
|
126
|
+
return {
|
|
127
|
+
policy_template: 'public_content',
|
|
128
|
+
selected_mode: 'public_read',
|
|
129
|
+
confidence: 'high',
|
|
130
|
+
evidence: buildEvidence({
|
|
131
|
+
intent,
|
|
132
|
+
selection,
|
|
133
|
+
preflight,
|
|
134
|
+
pageState,
|
|
135
|
+
handoff,
|
|
136
|
+
triggers: ['public_read_engine'],
|
|
137
|
+
}),
|
|
138
|
+
alternatives: [
|
|
139
|
+
{ mode: 'live_session', reason: 'browser_reuse_if_public_read_is_insufficient' },
|
|
140
|
+
],
|
|
141
|
+
fallback_chain: ['live_session'],
|
|
142
|
+
requires_human: false,
|
|
143
|
+
risk_level: 'low',
|
|
144
|
+
next_step: 'extract',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
policy_template: 'authenticated_content',
|
|
150
|
+
selected_mode: 'live_session',
|
|
151
|
+
confidence: preflight?.session_trust === 'high' ? 'high' : 'medium',
|
|
152
|
+
evidence: buildEvidence({
|
|
153
|
+
intent,
|
|
154
|
+
selection,
|
|
155
|
+
preflight,
|
|
156
|
+
pageState,
|
|
157
|
+
handoff,
|
|
158
|
+
triggers: ['runtime_default'],
|
|
159
|
+
}),
|
|
160
|
+
alternatives: [
|
|
161
|
+
{ mode: 'handoff', reason: 'human_required_if_runtime_progress_blocks' },
|
|
162
|
+
],
|
|
163
|
+
fallback_chain: ['handoff'],
|
|
164
|
+
requires_human: false,
|
|
165
|
+
risk_level: intent === 'act' ? 'medium' : 'low',
|
|
166
|
+
next_step: 'inspect',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { errorResponse, textResponse } from './responses.js';
|
|
2
|
+
|
|
3
|
+
const INSTANCE_CONFIRMATION_ERROR = 'INSTANCE_CONFIRMATION_REQUIRED';
|
|
4
|
+
|
|
5
|
+
function buildInstanceKey(instance = {}) {
|
|
6
|
+
return [
|
|
7
|
+
instance.display ?? 'unknown',
|
|
8
|
+
instance.browser ?? 'unknown',
|
|
9
|
+
instance.protocolVersion ?? 'unknown',
|
|
10
|
+
].join('|');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getRuntimeConfirmation(state) {
|
|
14
|
+
return state?.runtimeConfirmation ?? null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isRuntimeInstanceConfirmed(state, instance) {
|
|
18
|
+
const confirmation = getRuntimeConfirmation(state);
|
|
19
|
+
if (!confirmation || !instance) return false;
|
|
20
|
+
return confirmation.instance_key === buildInstanceKey(instance);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function storeRuntimeConfirmation(state, instance) {
|
|
24
|
+
const confirmation = {
|
|
25
|
+
instance_key: buildInstanceKey(instance),
|
|
26
|
+
display: instance?.display ?? 'unknown',
|
|
27
|
+
browser: instance?.browser ?? null,
|
|
28
|
+
protocolVersion: instance?.protocolVersion ?? null,
|
|
29
|
+
confirmed_at: Date.now(),
|
|
30
|
+
};
|
|
31
|
+
state.runtimeConfirmation = confirmation;
|
|
32
|
+
return confirmation;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getRuntimeConfirmationSummary(state, instance) {
|
|
36
|
+
const confirmation = getRuntimeConfirmation(state);
|
|
37
|
+
if (!confirmation) {
|
|
38
|
+
return { confirmed: false, reason: 'unconfirmed' };
|
|
39
|
+
}
|
|
40
|
+
if (!instance) {
|
|
41
|
+
return { confirmed: false, reason: 'instance_unavailable', confirmation };
|
|
42
|
+
}
|
|
43
|
+
if (confirmation.instance_key !== buildInstanceKey(instance)) {
|
|
44
|
+
return { confirmed: false, reason: 'instance_changed', confirmation };
|
|
45
|
+
}
|
|
46
|
+
return { confirmed: true, reason: 'confirmed', confirmation };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function requireConfirmedRuntimeInstance(state, instance, tool) {
|
|
50
|
+
if (!instance) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (isRuntimeInstanceConfirmed(state, instance)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const summary = getRuntimeConfirmationSummary(state, instance);
|
|
59
|
+
const reasonText = summary.reason === 'instance_changed'
|
|
60
|
+
? 'The runtime instance changed after the last confirmation.'
|
|
61
|
+
: 'The runtime instance has not been confirmed yet.';
|
|
62
|
+
|
|
63
|
+
return errorResponse([
|
|
64
|
+
'Runtime instance confirmation required.',
|
|
65
|
+
`Tool: ${tool}`,
|
|
66
|
+
...(instance?.browser ? [`Current browser: ${instance.browser}`] : []),
|
|
67
|
+
...(instance?.display ? [`Current instance mode: ${instance.display}`] : []),
|
|
68
|
+
reasonText,
|
|
69
|
+
'Call confirm_runtime_instance first, then retry the action.',
|
|
70
|
+
], {
|
|
71
|
+
error_code: INSTANCE_CONFIRMATION_ERROR,
|
|
72
|
+
retryable: true,
|
|
73
|
+
suggested_next_step: 'confirm_runtime_instance',
|
|
74
|
+
instance,
|
|
75
|
+
confirmation: summary,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function buildRuntimeConfirmationSuccessResponse(confirmation, instance) {
|
|
80
|
+
return textResponse([
|
|
81
|
+
`Runtime instance confirmed: ${confirmation.display}`,
|
|
82
|
+
...(instance?.browser ? [`Browser: ${instance.browser}`] : []),
|
|
83
|
+
], {
|
|
84
|
+
confirmation,
|
|
85
|
+
instance,
|
|
86
|
+
});
|
|
87
|
+
}
|