@mobileaidev/ai-app-bridge 0.2.15 → 0.3.0-rc.2

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 (156) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +306 -53
  4. package/bin/ai-app-bridge.js +51 -4504
  5. package/bin/android-permissions.js +152 -0
  6. package/bin/android-uia-xml.js +74 -0
  7. package/bin/artifact-paths.js +127 -1
  8. package/bin/bridge-forward.js +56 -0
  9. package/bin/command-discovery.js +86 -0
  10. package/bin/command-errors.js +60 -0
  11. package/bin/command-registry.js +504 -0
  12. package/bin/command-request.js +14 -0
  13. package/bin/command-router.js +80 -0
  14. package/bin/connection-cache.js +119 -0
  15. package/bin/device-provider.js +2934 -0
  16. package/bin/execution-host.js +596 -0
  17. package/bin/execution-runtime.js +101 -0
  18. package/bin/fact-codec.js +321 -0
  19. package/bin/fact-recorder.js +691 -0
  20. package/bin/fact-store.js +226 -0
  21. package/bin/feedback-probe.js +285 -0
  22. package/bin/intent/install-intent.js +267 -0
  23. package/bin/intent/intent-action-executor.js +129 -0
  24. package/bin/intent/intent-autonomous-adapter.js +46 -0
  25. package/bin/intent/intent-capture-port.js +64 -0
  26. package/bin/intent/intent-entry.js +226 -0
  27. package/bin/intent/intent-errors.js +28 -0
  28. package/bin/intent/intent-evidence-store.js +121 -0
  29. package/bin/intent/intent-lifetime.js +62 -0
  30. package/bin/intent/intent-observation-target.js +33 -0
  31. package/bin/intent/intent-observer.js +194 -0
  32. package/bin/intent/intent-production-adapter.js +334 -0
  33. package/bin/intent/intent-provider.js +27 -0
  34. package/bin/intent/intent-runtime.js +63 -0
  35. package/bin/intent/intent-worker.js +432 -0
  36. package/bin/intent/ios-intent-adapter.js +90 -0
  37. package/bin/intent/permission-intent.js +239 -0
  38. package/bin/intent/web-intent-adapter.js +31 -0
  39. package/bin/ios-device-outcome.js +47 -0
  40. package/bin/ios-execution.js +108 -0
  41. package/bin/ios-provider.js +497 -583
  42. package/bin/ios-runtime-binding.js +54 -0
  43. package/bin/ios-wda-execution.js +70 -0
  44. package/bin/ios-wda-port.js +98 -0
  45. package/bin/ios-wda-project.js +79 -0
  46. package/bin/mcp-server.js +83 -1040
  47. package/bin/mmap-scan-index.js +329 -0
  48. package/bin/observation-collector.js +862 -0
  49. package/bin/runtime-client.js +154 -0
  50. package/bin/runtime-directory.js +107 -0
  51. package/bin/runtime-protocol.js +36 -0
  52. package/bin/script/bounded-script-registry.js +103 -0
  53. package/bin/script/node-runtime-adapter.js +233 -0
  54. package/bin/script/progress-projector.js +63 -0
  55. package/bin/script/python-runtime-adapter.js +111 -0
  56. package/bin/script/rolling-summary.js +134 -0
  57. package/bin/script/script-agent-port.js +40 -0
  58. package/bin/script/script-assert.js +95 -0
  59. package/bin/script/script-capture-port.js +76 -0
  60. package/bin/script/script-catalog.js +85 -0
  61. package/bin/script/script-durable-restore.js +195 -0
  62. package/bin/script/script-entry-code.js +26 -0
  63. package/bin/script/script-entry-route.js +38 -0
  64. package/bin/script/script-entry.js +3 -0
  65. package/bin/script/script-errors.js +29 -0
  66. package/bin/script/script-evidence-store.js +22 -0
  67. package/bin/script/script-format-removed.js +26 -0
  68. package/bin/script/script-host-port.js +397 -0
  69. package/bin/script/script-ledger.js +64 -0
  70. package/bin/script/script-result.js +57 -0
  71. package/bin/script/script-sdk.js +152 -0
  72. package/bin/script/script-sdk.py +153 -0
  73. package/bin/script/script-session-channel.js +127 -0
  74. package/bin/script/script-spec.js +86 -0
  75. package/bin/script/script-supervisor.js +919 -0
  76. package/bin/script/templates/checkpoint-reentry.js +13 -0
  77. package/bin/segment-index.js +481 -0
  78. package/bin/segmented-fact-store.js +1571 -0
  79. package/bin/shared-kernel/android-h5-target.js +10 -0
  80. package/bin/shared-kernel/android-install-execution.js +176 -0
  81. package/bin/shared-kernel/android-sdk-endpoint.js +42 -0
  82. package/bin/shared-kernel/android-shell-execution.js +195 -0
  83. package/bin/shared-kernel/argument-schema.js +117 -0
  84. package/bin/shared-kernel/canonical-path.js +17 -0
  85. package/bin/shared-kernel/device-acknowledgements.js +53 -0
  86. package/bin/shared-kernel/device-completion-history.js +52 -0
  87. package/bin/shared-kernel/device-mutation-lease.js +219 -0
  88. package/bin/shared-kernel/device-ownership-recovery.js +95 -0
  89. package/bin/shared-kernel/device-ownership-store.js +94 -0
  90. package/bin/shared-kernel/evidence-adapters.js +251 -0
  91. package/bin/shared-kernel/evidence-archive.js +329 -0
  92. package/bin/shared-kernel/evidence-recording.js +131 -0
  93. package/bin/shared-kernel/evidence-schema.js +194 -0
  94. package/bin/shared-kernel/evidence-store.js +190 -0
  95. package/bin/shared-kernel/execution-admission.js +22 -0
  96. package/bin/shared-kernel/execution-contracts.js +171 -0
  97. package/bin/shared-kernel/execution-io.js +106 -0
  98. package/bin/shared-kernel/execution-ledger.js +125 -0
  99. package/bin/shared-kernel/execution-scope.js +87 -0
  100. package/bin/shared-kernel/execution-target.js +115 -0
  101. package/bin/shared-kernel/flutter-execution.js +13 -0
  102. package/bin/shared-kernel/flutter-h5-port.js +60 -0
  103. package/bin/shared-kernel/flutter-h5-target.js +9 -0
  104. package/bin/shared-kernel/flutter-target.js +75 -0
  105. package/bin/shared-kernel/h5-execution.js +11 -0
  106. package/bin/shared-kernel/h5-target.js +31 -0
  107. package/bin/shared-kernel/host-fact-store.js +49 -0
  108. package/bin/shared-kernel/ios-h5-target.js +9 -0
  109. package/bin/shared-kernel/ios-native-target.js +71 -0
  110. package/bin/shared-kernel/live-capture-query.js +115 -0
  111. package/bin/shared-kernel/managed-sdk-execution.js +78 -0
  112. package/bin/shared-kernel/native-execution.js +13 -0
  113. package/bin/shared-kernel/native-target.js +156 -0
  114. package/bin/shared-kernel/provider-command-contracts.js +55 -0
  115. package/bin/shared-kernel/recorded-payload-archive.js +195 -0
  116. package/bin/shared-kernel/request-context.js +35 -0
  117. package/bin/shared-kernel/semantic-node.js +55 -0
  118. package/bin/shared-kernel/summary-transformer.js +352 -0
  119. package/bin/shared-kernel/target-lease-protocol.js +47 -0
  120. package/bin/shared-kernel/text-wait.js +111 -0
  121. package/bin/shared-kernel/uia-execution.js +96 -0
  122. package/bin/shared-kernel/uia-protocol.js +214 -0
  123. package/bin/shared-kernel/uia-runtime-port.js +377 -0
  124. package/bin/shared-kernel/uia-target.js +39 -0
  125. package/bin/shared-kernel/web-dom-target.js +44 -0
  126. package/bin/shared-kernel/xml-attributes.js +25 -0
  127. package/bin/target-execution.js +275 -0
  128. package/bin/web/command-schema.js +60 -0
  129. package/bin/web/session-store.js +157 -0
  130. package/bin/web-provider.js +334 -553
  131. package/docs/COMMAND_CONTRACT.md +1563 -0
  132. package/docs/EVIDENCE_ARCHIVE.md +214 -0
  133. package/docs/INTENT_FOREGROUND.md +71 -0
  134. package/docs/INTENT_NATIVE_EDITING.md +79 -0
  135. package/docs/RELEASE.md +59 -0
  136. package/docs/SCRIPT_AUTHORING.md +489 -0
  137. package/node_modules/@mobileaidev/segmented-fact-store-native/LICENSE +201 -0
  138. package/node_modules/@mobileaidev/segmented-fact-store-native/NOTICE +7 -0
  139. package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +36 -0
  140. package/node_modules/@mobileaidev/segmented-fact-store-native/bindings/node/sfs_node.c +597 -0
  141. package/node_modules/@mobileaidev/segmented-fact-store-native/include/sfs.h +178 -0
  142. package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +5 -0
  143. package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +24 -0
  144. package/node_modules/@mobileaidev/segmented-fact-store-native/src/sfs.c +2349 -0
  145. package/package.json +60 -5
  146. package/runtime/ios-wda/AABWDABinding.h +19 -0
  147. package/runtime/ios-wda/AABWDABinding.m +97 -0
  148. package/runtime/ios-wda/AABWDAExecution.h +26 -0
  149. package/runtime/ios-wda/AABWDAExecution.m +172 -0
  150. package/runtime/ios-wda/AABWDAIntegration.h +71 -0
  151. package/runtime/ios-wda/AABWDAManagedRoutes.h +392 -0
  152. package/runtime/ios-wda/AABWDAReceiptStore.h +10 -0
  153. package/runtime/ios-wda/AABWDAReceiptStore.m +116 -0
  154. package/runtime/uia/ai-app-bridge-uia.jar +0 -0
  155. package/runtime/uia/manifest.json +22 -0
  156. package/skills/ai-app-bridge-use/SKILL.md +23 -360
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ function createProgressProjector({ maxEvents = 256 } = {}) {
4
+ const executions = new Map();
5
+
6
+ function accept(fact) {
7
+ if (!fact || typeof fact.executionId !== 'string' || fact.executionId.length === 0) {
8
+ throw new TypeError('executionId');
9
+ }
10
+ let record = executions.get(fact.executionId);
11
+ if (!record) {
12
+ record = { sequence: 0, events: [], business: null };
13
+ executions.set(fact.executionId, record);
14
+ }
15
+ record.sequence += 1;
16
+ const stored = {
17
+ schemaVersion: 'aab.execution-fact/v1',
18
+ executionId: fact.executionId,
19
+ sequence: record.sequence,
20
+ revision: fact.revision,
21
+ kind: fact.kind,
22
+ target: fact.target,
23
+ timestampMs: fact.timestampMs,
24
+ actionId: fact.actionId,
25
+ parentFactId: fact.parentFactId,
26
+ payloadSummary: fact.payloadSummary,
27
+ evidenceRefs: fact.evidenceRefs,
28
+ timings: fact.timings,
29
+ };
30
+ record.events.push(stored);
31
+ while (record.events.length > maxEvents) {
32
+ record.events.shift();
33
+ }
34
+ if (fact.kind === 'progress') {
35
+ record.business = stored;
36
+ }
37
+ return {
38
+ lastSequence: record.sequence,
39
+ lastKind: stored.kind,
40
+ business: record.business,
41
+ };
42
+ }
43
+
44
+ function read(executionId, afterSequence, limit) {
45
+ const record = executions.get(executionId);
46
+ if (!record) {
47
+ return { items: [], lastSequence: 0, hasMore: false };
48
+ }
49
+ const start = afterSequence;
50
+ const cap = limit;
51
+ const items = record.events.filter((event) => event.sequence > start);
52
+ const limited = items.length > cap ? items.slice(0, cap) : items;
53
+ return {
54
+ items: limited,
55
+ lastSequence: record.sequence,
56
+ hasMore: items.length > limited.length,
57
+ };
58
+ }
59
+
60
+ return { accept, read };
61
+ }
62
+
63
+ module.exports = { createProgressProjector };
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ const { spawnSync } = require('node:child_process');
4
+ const { createChildRuntimeAdapter } = require('./node-runtime-adapter');
5
+
6
+ let cached = undefined;
7
+ let spawnFailureReprobed = false;
8
+
9
+ function inspectPython(opts = {}) {
10
+ if (opts.resolvePython) return detectPython(opts.resolvePython, opts);
11
+ if (opts.pythonPath || opts.env) return detectPython(undefined, opts);
12
+ if (cached !== undefined) return cached;
13
+ cached = detectPython(undefined, opts);
14
+ return cached;
15
+ }
16
+
17
+ function resetPythonDetection() {
18
+ cached = undefined;
19
+ spawnFailureReprobed = false;
20
+ }
21
+
22
+ function pythonCandidates({ pythonPath, env = process.env } = {}) {
23
+ const seen = new Set();
24
+ const list = [];
25
+ function add(value) {
26
+ if (!value || seen.has(value)) return;
27
+ seen.add(value);
28
+ list.push(value);
29
+ }
30
+ add(pythonPath);
31
+ add(env.AI_APP_BRIDGE_PYTHON);
32
+ add('python3');
33
+ add('python');
34
+ return list;
35
+ }
36
+
37
+ function probePython(executable, env) {
38
+ const options = { encoding: 'utf8', shell: false };
39
+ if (env) options.env = env;
40
+ const probe = spawnSync(executable, ['-c', 'import sys; print("%d.%d" % sys.version_info[:2])'], options);
41
+ if (probe.status !== 0 || !probe.stdout) {
42
+ return { available: false, executable, error: 'runtime_unavailable' };
43
+ }
44
+ const parsed = parsePythonVersion(probe.stdout);
45
+ if (!parsed) {
46
+ return { available: false, executable, error: 'runtime_unavailable' };
47
+ }
48
+ if (parsed.major < 3 || (parsed.major === 3 && parsed.minor < 9)) {
49
+ return { available: false, executable, version: parsed.version, error: 'python_too_old' };
50
+ }
51
+ return { available: true, executable, version: parsed.version };
52
+ }
53
+
54
+ function parsePythonVersion(stdout) {
55
+ const version = String(stdout || '').trim();
56
+ const match = /^(\d+)\.(\d+)\b/.exec(version);
57
+ if (!match) return null;
58
+ return { major: Number(match[1]), minor: Number(match[2]), version };
59
+ }
60
+
61
+ function detectPython(resolvePython, opts = {}) {
62
+ if (typeof resolvePython === 'function') {
63
+ const executable = resolvePython();
64
+ if (!executable) return { available: false, executable: null };
65
+ return probePython(executable, opts.env);
66
+ }
67
+ let tooOld = null;
68
+ for (const candidate of pythonCandidates(opts)) {
69
+ const result = probePython(candidate, opts.env);
70
+ if (result.available) return result;
71
+ if (result.error === 'python_too_old' && !tooOld) tooOld = result;
72
+ }
73
+ return tooOld || { available: false, executable: null };
74
+ }
75
+
76
+ function resolvePythonExecutable(opts = {}) {
77
+ return detectPython(undefined, opts).executable;
78
+ }
79
+
80
+ function createPythonRuntimeAdapter(opts = {}) {
81
+ const resolved = inspectPython(opts);
82
+ if (!resolved.available) {
83
+ return {
84
+ kind: 'python',
85
+ unavailable: true,
86
+ error: resolved.error || 'runtime_unavailable',
87
+ async start() {
88
+ return { ok: false, error: 'runtime_unavailable' };
89
+ },
90
+ };
91
+ }
92
+ return createChildRuntimeAdapter({
93
+ kind: 'python',
94
+ executable: resolved.executable,
95
+ sdkFile: 'script-sdk.py',
96
+ extension: '.py',
97
+ onSpawnFailure() {
98
+ if (spawnFailureReprobed) return;
99
+ spawnFailureReprobed = true;
100
+ cached = undefined;
101
+ },
102
+ });
103
+ }
104
+
105
+ module.exports = {
106
+ createPythonRuntimeAdapter,
107
+ inspectPython,
108
+ resetPythonDetection,
109
+ resolvePythonExecutable,
110
+ detectPython,
111
+ };
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ function createRollingSummary({ operationId, startedAtMs }) {
4
+ const assertions = { passed: 0, failed: 0, inconclusive: 0 };
5
+ const assertionScopes = {
6
+ code: { passed: 0, failed: 0, inconclusive: 0 },
7
+ device: { passed: 0, failed: 0, inconclusive: 0 },
8
+ };
9
+ let status = 'running';
10
+ let stage = null;
11
+ let message = null;
12
+ let currentCall = null;
13
+ let completedCalls = 0;
14
+ let lastCheckpoint = null;
15
+ let lastEventAtMs = startedAtMs;
16
+ let lastKind = null;
17
+ let latestEvidenceRefs = [];
18
+ let agentRequest = null;
19
+ let pausedAtMs = null;
20
+ let pausedMs = 0;
21
+ let terminalAtMs = null;
22
+
23
+ function apply(event) {
24
+ if (event.kind === 'heartbeat') {
25
+ return snapshot(event.timestampMs);
26
+ }
27
+ lastEventAtMs = event.timestampMs;
28
+ lastKind = event.kind;
29
+ if (event.evidenceRefs !== undefined) {
30
+ latestEvidenceRefs = event.evidenceRefs;
31
+ }
32
+ if (event.kind === 'call_started') {
33
+ currentCall = event.command;
34
+ }
35
+ if (event.kind === 'call_completed' || event.kind === 'call_failed') {
36
+ completedCalls += 1;
37
+ currentCall = null;
38
+ }
39
+ if (event.kind === 'assertion_passed') {
40
+ assertions.passed += 1;
41
+ }
42
+ if (event.kind === 'assertion_failed') {
43
+ assertions.failed += 1;
44
+ }
45
+ if (event.kind === 'assertion_inconclusive') {
46
+ assertions.inconclusive += 1;
47
+ }
48
+ if (event.kind.startsWith('assertion_')) {
49
+ const verdict = event.kind.slice('assertion_'.length);
50
+ const scope = event.scope || 'device';
51
+ if (assertionScopes[scope] && Object.hasOwn(assertionScopes[scope], verdict)) assertionScopes[scope][verdict] += 1;
52
+ }
53
+ if (event.kind === 'checkpoint_committed') {
54
+ lastCheckpoint = event.name;
55
+ }
56
+ if (event.kind === 'progress') {
57
+ stage = event.stage;
58
+ message = event.message;
59
+ }
60
+ if (event.kind === 'agent_question_created') {
61
+ agentRequest = event.request;
62
+ }
63
+ if (event.kind === 'agent_decision') {
64
+ agentRequest = null;
65
+ }
66
+ if (event.kind === 'paused') {
67
+ status = 'paused';
68
+ pausedAtMs = event.timestampMs;
69
+ }
70
+ if (event.kind === 'resumed') {
71
+ status = 'running';
72
+ if (pausedAtMs !== null) {
73
+ pausedMs += event.timestampMs - pausedAtMs;
74
+ pausedAtMs = null;
75
+ }
76
+ agentRequest = null;
77
+ }
78
+ if (event.kind === 'script_completed') {
79
+ status = 'completed';
80
+ }
81
+ if (event.kind === 'script_failed') {
82
+ status = 'failed';
83
+ }
84
+ if (event.kind === 'script_cancelled') {
85
+ status = 'cancelled';
86
+ }
87
+ if (event.kind === 'script_ambiguous') {
88
+ status = 'ambiguous';
89
+ }
90
+ if (['script_completed', 'script_failed', 'script_cancelled', 'script_ambiguous'].includes(event.kind)
91
+ && terminalAtMs === null) terminalAtMs = event.timestampMs;
92
+ return snapshot(event.timestampMs);
93
+ }
94
+
95
+ function snapshot(nowMs) {
96
+ const endAtMs = terminalAtMs ?? nowMs;
97
+ const holdMs = pausedAtMs === null ? 0 : endAtMs - pausedAtMs;
98
+ return {
99
+ operationId,
100
+ status,
101
+ stage,
102
+ message,
103
+ currentCall,
104
+ completedCalls,
105
+ assertions: {
106
+ passed: assertions.passed,
107
+ failed: assertions.failed,
108
+ inconclusive: assertions.inconclusive,
109
+ },
110
+ assertionScopes: { code: { ...assertionScopes.code }, device: { ...assertionScopes.device } },
111
+ lastCheckpoint,
112
+ elapsedMs: endAtMs - startedAtMs,
113
+ activeMs: endAtMs - startedAtMs - pausedMs - holdMs,
114
+ decisionWaitMs: agentRequest === null ? 0 : endAtMs - lastEventAtMs,
115
+ latestEvidenceRefs,
116
+ agentRequest,
117
+ };
118
+ }
119
+
120
+ function heartbeat(nowMs, childHealth) {
121
+ return {
122
+ kind: 'heartbeat',
123
+ timestampMs: nowMs,
124
+ elapsedMs: (terminalAtMs ?? nowMs) - startedAtMs,
125
+ lastEventAtMs,
126
+ lastKind,
127
+ childHealth,
128
+ };
129
+ }
130
+
131
+ return { apply, snapshot, heartbeat };
132
+ }
133
+
134
+ module.exports = { createRollingSummary };
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ function createScriptAgentPort() {
4
+ const pending = [];
5
+
6
+ function askAgent(request) {
7
+ const queued = {
8
+ requestId: (request && request.requestId) || `ask-${pending.length + 1}`,
9
+ request,
10
+ answered: false,
11
+ };
12
+ pending.push(queued);
13
+ return new Promise((resolve) => {
14
+ queued.resolve = resolve;
15
+ });
16
+ }
17
+
18
+ function decide(requestId, decision) {
19
+ const open = pending.find((entry) => entry.requestId === requestId && entry.answered === false);
20
+ if (open) {
21
+ open.answered = true;
22
+ open.decision = decision;
23
+ if (open.resolve) open.resolve(decision);
24
+ return { ok: true, requestId, decision };
25
+ }
26
+ const answered = pending.find((entry) => entry.requestId === requestId && entry.answered === true);
27
+ if (answered) {
28
+ return { ok: true, requestId, decision: answered.decision };
29
+ }
30
+ return { ok: false, error: 'unknown_or_duplicate_decide', requestId };
31
+ }
32
+
33
+ return {
34
+ askAgent,
35
+ decide,
36
+ pending: () => pending.filter((item) => item.answered === false),
37
+ };
38
+ }
39
+
40
+ module.exports = { createScriptAgentPort };
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ const { randomUUID, createHash } = require('node:crypto');
4
+ const { isDeepStrictEqual } = require('node:util');
5
+
6
+ // The registry is an execution-local verifier, never a second mobile payload store.
7
+ function createObservationRegistry({ maxCount = 128, maxBytes = 256 * 1024 } = {}) {
8
+ const entries = new Map();
9
+ let retainedBytes = 0;
10
+ return {
11
+ set(id, record) {
12
+ const bytes = Buffer.byteLength(JSON.stringify(record));
13
+ const previous = entries.get(id);
14
+ if (previous) { retainedBytes -= previous.bytes; entries.delete(id); }
15
+ if (bytes > maxBytes) return;
16
+ while (entries.size >= maxCount || retainedBytes + bytes > maxBytes) {
17
+ const oldest = entries.keys().next().value;
18
+ retainedBytes -= entries.get(oldest).bytes;
19
+ entries.delete(oldest);
20
+ }
21
+ entries.set(id, { record, bytes });
22
+ retainedBytes += bytes;
23
+ },
24
+ get(id) { return entries.get(id)?.record; },
25
+ entries() { return [...entries].map(([id, item]) => [id, item.record]); },
26
+ get retainedCount() { return entries.size; },
27
+ get retainedBytes() { return retainedBytes; },
28
+ };
29
+ }
30
+
31
+ // Stored metadata belongs to the Host. The child only receives a JSON copy.
32
+ function issueObservation(observations, evidence, { afterActionId, mutationRevision, pendingMutation, captureValid }, { command, payload, tree = false }) {
33
+ evidence.observationId = randomUUID();
34
+ evidence.window.closedAtMs = Date.now();
35
+ evidence.source = {
36
+ scope: 'execution',
37
+ command,
38
+ payloadSha256: createHash('sha256').update(JSON.stringify(payload ?? null)).digest('hex'),
39
+ };
40
+ if (tree && evidence.refs.length === 0) {
41
+ evidence.refs = [{ stream: 'tree', hostObservationId: evidence.observationId, ...evidence.source }];
42
+ }
43
+ observations.set(evidence.observationId, {
44
+ evidence: structuredClone(evidence),
45
+ afterActionId,
46
+ mutationRevision,
47
+ pendingMutation,
48
+ captureValid,
49
+ });
50
+ return evidence;
51
+ }
52
+
53
+ function judgeAssertion(assertion, observations, { afterActionId = null, mutationRevision = 0, pendingMutation = false } = {}) {
54
+ const scope = assertion.scope || 'device';
55
+ const result = (verdict, reason) => ({
56
+ verdict,
57
+ name: assertion.name || null,
58
+ scope,
59
+ ...(reason ? { reason } : {}),
60
+ });
61
+ if (scope !== 'device' && scope !== 'code') return result('inconclusive', 'assertion_scope_invalid');
62
+ if (typeof assertion.condition !== 'boolean') return result('inconclusive', 'assertion_condition_not_boolean');
63
+ if (scope === 'code') {
64
+ if (assertion.evidence != null || (assertion.requiredEvidence || []).length > 0) {
65
+ return result('inconclusive', 'code_assertion_cannot_claim_device_evidence');
66
+ }
67
+ return result(assertion.condition ? 'passed' : 'failed');
68
+ }
69
+ const evidence = assertion.evidence;
70
+ const issued = observations.get(evidence?.observationId);
71
+ if (!issued || !isDeepStrictEqual(evidence, issued.evidence)) {
72
+ return result('inconclusive', 'evidence_not_host_issued');
73
+ }
74
+ const coverage = issued.evidence.coverage;
75
+ if (coverage.status !== 'complete' || coverage.gap !== false || coverage.committed !== true) {
76
+ return result('inconclusive', 'evidence_coverage_incomplete');
77
+ }
78
+ if (
79
+ pendingMutation || issued.pendingMutation
80
+ || issued.mutationRevision !== mutationRevision
81
+ || issued.afterActionId !== afterActionId
82
+ ) {
83
+ return result('inconclusive', 'evidence_action_window_stale');
84
+ }
85
+ if (issued.captureValid === false) return result('inconclusive', 'capture_boundary_not_host_observed');
86
+ const refs = issued.evidence.refs;
87
+ if (!Array.isArray(refs) || refs.length === 0) return result('inconclusive', 'device_evidence_refs_empty');
88
+ const streams = new Set(refs.map((ref) => ref.stream));
89
+ if ((assertion.requiredEvidence || []).some((stream) => !streams.has(stream))) {
90
+ return result('inconclusive', 'required_evidence_missing');
91
+ }
92
+ return result(assertion.condition ? 'passed' : 'failed');
93
+ }
94
+
95
+ module.exports = { createObservationRegistry, issueObservation, judgeAssertion };
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ const { capturePage, unavailablePage } = require('../shared-kernel/live-capture-query');
4
+
5
+ function createScriptCapturePort({ query } = {}) {
6
+ if (typeof query !== 'function') {
7
+ throw new TypeError('query');
8
+ }
9
+
10
+ async function queryWindow(commandName, args = {}, executionContext = {}) {
11
+ const stream = streamFor(commandName);
12
+ if (!stream) {
13
+ return unavailablePage('unsupported_capture_stream');
14
+ }
15
+ if (executionContext.runtimeEpochChanged === true || executionContext.disconnected === true) {
16
+ return unavailablePage(executionContext.disconnected ? 'target_disconnected' : 'runtime_epoch_changed');
17
+ }
18
+ const page = await query({
19
+ ...args,
20
+ command: commandName,
21
+ view: args.view ?? (args.history === true ? 'connected-history' : 'decision-window'),
22
+ stream,
23
+ afterActionId: afterActionIdOf(args, executionContext),
24
+ timeoutMs: timeoutMsOf(args, executionContext),
25
+ runtimeEpoch: executionContext.runtimeEpoch ?? args.runtimeEpoch ?? null,
26
+ sinceMs: args.sinceMs ?? null,
27
+ limit: args.limit ?? (commandName.startsWith('web-') ? 16 : 200),
28
+ serial: args.serial,
29
+ packageName: args.packageName,
30
+ });
31
+ return capturePage(page);
32
+ }
33
+
34
+ return { query: queryWindow };
35
+ }
36
+
37
+ function afterActionIdOf(args, executionContext) {
38
+ if (executionContext.evidenceWindow && executionContext.evidenceWindow.afterActionId != null) {
39
+ return executionContext.evidenceWindow.afterActionId;
40
+ }
41
+ if (executionContext.actionId != null) return executionContext.actionId;
42
+ if (args.afterActionId != null) return args.afterActionId;
43
+ return null;
44
+ }
45
+
46
+ function timeoutMsOf(args, executionContext) {
47
+ if (executionContext.evidenceWindow && executionContext.evidenceWindow.timeoutMs != null) {
48
+ return executionContext.evidenceWindow.timeoutMs;
49
+ }
50
+ if (executionContext.timeoutMs != null) return executionContext.timeoutMs;
51
+ return args.timeoutMs ?? null;
52
+ }
53
+
54
+ function streamFor(commandName) {
55
+ if (
56
+ commandName === 'logs'
57
+ || commandName === 'ios-logs'
58
+ || commandName === 'web-logs'
59
+ || commandName === 'webview-console'
60
+ ) return 'logs';
61
+ if (
62
+ commandName === 'network'
63
+ || commandName === 'ios-network'
64
+ || commandName === 'web-network'
65
+ || commandName === 'webview-network'
66
+ ) return 'network';
67
+ if (commandName === 'state' || commandName === 'ios-state' || commandName === 'web-state') return 'state';
68
+ if (commandName === 'events' || commandName === 'ios-events' || commandName === 'web-events') return 'events';
69
+ return null;
70
+ }
71
+
72
+ function isCaptureReadCommand(commandName) {
73
+ return streamFor(commandName) !== null;
74
+ }
75
+
76
+ module.exports = { createScriptCapturePort, isCaptureReadCommand };
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const { scriptPermissions } = require('../command-registry');
4
+ const PERMISSIONS = Object.freeze(Object.fromEntries(Object.entries(scriptPermissions).map(([name, commands]) =>
5
+ [name, Object.freeze(name === 'app.read' ? [...commands, 'page-summary'] : [...commands])])));
6
+
7
+ const DENY_DEFAULT = Object.freeze([
8
+ 'clear-app-data',
9
+ 'install-apk',
10
+ 'ios-install-app',
11
+ 'ios-setup',
12
+ 'permission-grant',
13
+ 'permission-revoke',
14
+ 'appops-set',
15
+ ]);
16
+
17
+ const DENY_PERMANENT = Object.freeze([
18
+ 'h5-eval',
19
+ 'flutter-h5-eval',
20
+ 'ios-h5-eval',
21
+ 'web-command',
22
+ 'forward',
23
+ 'remove-forward',
24
+ ]);
25
+
26
+ const DEFAULT_PERMISSIONS = Object.freeze(['app.read', 'app.interact', 'capture.read']);
27
+
28
+ function allowedCommands(permissions = DEFAULT_PERMISSIONS) {
29
+ const names = new Set();
30
+ for (const permission of permissions) {
31
+ for (const command of PERMISSIONS[permission] || []) names.add(command);
32
+ }
33
+ return [...names];
34
+ }
35
+
36
+ function commandPermission(command) {
37
+ for (const [permission, commands] of Object.entries(PERMISSIONS)) {
38
+ if (commands.includes(command)) return permission;
39
+ }
40
+ return null;
41
+ }
42
+
43
+ function authorizeCommand(command, permissions = DEFAULT_PERMISSIONS) {
44
+ if (DENY_PERMANENT.includes(command)) {
45
+ return { ok: false, error: 'command_permanently_denied', command };
46
+ }
47
+ if (DENY_DEFAULT.includes(command) && !permissions.includes(commandPermission(command))) {
48
+ return { ok: false, error: 'command_not_in_default_allowlist', command };
49
+ }
50
+ const permission = commandPermission(command);
51
+ if (!permission) {
52
+ return { ok: false, error: 'command_not_in_catalog', command };
53
+ }
54
+ if (!permissions.includes(permission)) {
55
+ return { ok: false, error: 'permission_not_granted', command, permission };
56
+ }
57
+ return { ok: true, command, permission };
58
+ }
59
+
60
+ function catalogPayload() {
61
+ return {
62
+ runtime: 'trusted-local-code',
63
+ warning: 'Script source is trusted-local-code, not an OS sandbox. permissions gate Bridge SDK calls only.',
64
+ schemaVersion: 'aab.code-script/v1',
65
+ languages: ['javascript', 'python'],
66
+ executablePlatforms: ['android', 'ios', 'web'],
67
+ unavailablePlatforms: {},
68
+ entrypoint: 'main',
69
+ permissions: PERMISSIONS,
70
+ defaultPermissions: DEFAULT_PERMISSIONS,
71
+ denyDefault: DENY_DEFAULT,
72
+ denyPermanent: DENY_PERMANENT,
73
+ internalOnly: ['page-summary'],
74
+ };
75
+ }
76
+
77
+ module.exports = {
78
+ PERMISSIONS,
79
+ DENY_DEFAULT,
80
+ DENY_PERMANENT,
81
+ DEFAULT_PERMISSIONS,
82
+ allowedCommands,
83
+ authorizeCommand,
84
+ catalogPayload,
85
+ };