presubmit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +51 -0
- package/THIRD_PARTY_NOTICES.md +24 -0
- package/apps/web/dist/assets/FileCode-CZZH52o7.js +5 -0
- package/apps/web/dist/assets/FileCode-DovDO7Jh.css +1 -0
- package/apps/web/dist/assets/SettingsPage-CRvOwdjt.js +4 -0
- package/apps/web/dist/assets/SettingsPage-UbYSCwJj.css +1 -0
- package/apps/web/dist/assets/TerminalPanel-0sw-J6YS.css +1 -0
- package/apps/web/dist/assets/TerminalPanel-BqUjPWav.js +16 -0
- package/apps/web/dist/assets/index-BhqNNFja.js +54 -0
- package/apps/web/dist/assets/index-DD8VXy8H.css +1 -0
- package/apps/web/dist/assets/presubmit-mark-BBQ19Fmr.svg +7 -0
- package/apps/web/dist/assets/presubmit-mark-busy-CwBC-7on.svg +8 -0
- package/apps/web/dist/assets/react-vendor-wiHys0m2.js +9 -0
- package/apps/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/apps/web/dist/index.html +6 -0
- package/apps/web/dist/service-worker.js +11 -0
- package/dist/apps/server/src/agent-builder.js +204 -0
- package/dist/apps/server/src/attach.js +163 -0
- package/dist/apps/server/src/attachments.js +86 -0
- package/dist/apps/server/src/browser-access.js +131 -0
- package/dist/apps/server/src/check-provenance.js +95 -0
- package/dist/apps/server/src/check-supervisor.js +112 -0
- package/dist/apps/server/src/checks.js +402 -0
- package/dist/apps/server/src/child-collaboration-guidance.js +2 -0
- package/dist/apps/server/src/child-sessions.js +191 -0
- package/dist/apps/server/src/cli.js +162 -0
- package/dist/apps/server/src/conversation-search.js +12 -0
- package/dist/apps/server/src/coordinator-messages.js +72 -0
- package/dist/apps/server/src/dev-auth.js +54 -0
- package/dist/apps/server/src/dev-server.js +201 -0
- package/dist/apps/server/src/dev-worker.js +48 -0
- package/dist/apps/server/src/diagnostics.js +105 -0
- package/dist/apps/server/src/fast-mode.js +8 -0
- package/dist/apps/server/src/folder-picker.js +54 -0
- package/dist/apps/server/src/git-blobs.js +62 -0
- package/dist/apps/server/src/git-handoff.js +127 -0
- package/dist/apps/server/src/history-sources.js +59 -0
- package/dist/apps/server/src/hub.js +159 -0
- package/dist/apps/server/src/internal-transport.js +38 -0
- package/dist/apps/server/src/local-embeddings.js +70 -0
- package/dist/apps/server/src/ollama-setup.js +56 -0
- package/dist/apps/server/src/planning.js +30 -0
- package/dist/apps/server/src/project-agents.js +276 -0
- package/dist/apps/server/src/project-memory.js +94 -0
- package/dist/apps/server/src/projects.js +106 -0
- package/dist/apps/server/src/queued-launch.js +13 -0
- package/dist/apps/server/src/reply-search.js +172 -0
- package/dist/apps/server/src/review-context.js +60 -0
- package/dist/apps/server/src/review-refresh.js +14 -0
- package/dist/apps/server/src/reviewers.js +75 -0
- package/dist/apps/server/src/search-settings.js +22 -0
- package/dist/apps/server/src/semantic-history.js +186 -0
- package/dist/apps/server/src/server.js +4338 -0
- package/dist/apps/server/src/session-changes.js +181 -0
- package/dist/apps/server/src/session-router-settings.js +198 -0
- package/dist/apps/server/src/session-workspaces.js +135 -0
- package/dist/apps/server/src/sessions.js +579 -0
- package/dist/apps/server/src/shared-auth.js +70 -0
- package/dist/apps/server/src/shell-evaluator-settings.js +53 -0
- package/dist/apps/server/src/store.js +203 -0
- package/dist/apps/server/src/tailscale-process.js +115 -0
- package/dist/apps/server/src/tailscale-serve-worker.js +33 -0
- package/dist/apps/server/src/tailscale-serve.js +276 -0
- package/dist/apps/server/src/task-capacity.js +92 -0
- package/dist/apps/server/src/task-graph.js +190 -0
- package/dist/apps/server/src/task-logs.js +74 -0
- package/dist/apps/server/src/task-numbers.js +25 -0
- package/dist/apps/server/src/task-orchestrator.js +1088 -0
- package/dist/apps/server/src/terminal.js +182 -0
- package/dist/apps/server/src/terminals.js +80 -0
- package/dist/apps/server/src/tool-safety.js +2 -0
- package/dist/apps/server/src/workspace-lock.js +140 -0
- package/dist/apps/server/src/workspace-watcher.js +65 -0
- package/dist/apps/server/src/workspace.js +517 -0
- package/dist/packages/agent-pi/src/agent-builder-tool.js +32 -0
- package/dist/packages/agent-pi/src/agent-capabilities.js +44 -0
- package/dist/packages/agent-pi/src/ask-user.js +86 -0
- package/dist/packages/agent-pi/src/auth.js +510 -0
- package/dist/packages/agent-pi/src/check-tools.js +72 -0
- package/dist/packages/agent-pi/src/command-policy.js +246 -0
- package/dist/packages/agent-pi/src/coordinator-tools.js +122 -0
- package/dist/packages/agent-pi/src/fast-mode.js +67 -0
- package/dist/packages/agent-pi/src/file-tools.js +163 -0
- package/dist/packages/agent-pi/src/history-tool.js +40 -0
- package/dist/packages/agent-pi/src/history.js +34 -0
- package/dist/packages/agent-pi/src/index.js +177 -0
- package/dist/packages/agent-pi/src/ipc.js +314 -0
- package/dist/packages/agent-pi/src/jev-session-router.js +408 -0
- package/dist/packages/agent-pi/src/jev-tools.js +71 -0
- package/dist/packages/agent-pi/src/local-model.js +277 -0
- package/dist/packages/agent-pi/src/memory-tools.js +119 -0
- package/dist/packages/agent-pi/src/model.js +25 -0
- package/dist/packages/agent-pi/src/planning.js +206 -0
- package/dist/packages/agent-pi/src/resource-loader.js +23 -0
- package/dist/packages/agent-pi/src/review-tools.js +158 -0
- package/dist/packages/agent-pi/src/review-transition.js +73 -0
- package/dist/packages/agent-pi/src/review.js +40 -0
- package/dist/packages/agent-pi/src/routing.js +27 -0
- package/dist/packages/agent-pi/src/session-setup.js +118 -0
- package/dist/packages/agent-pi/src/shell-context.js +51 -0
- package/dist/packages/agent-pi/src/shell-policy.js +281 -0
- package/dist/packages/agent-pi/src/shell-safety.js +205 -0
- package/dist/packages/agent-pi/src/shell.js +121 -0
- package/dist/packages/agent-pi/src/snapshot-permissions.js +12 -0
- package/dist/packages/agent-pi/src/spike.js +41 -0
- package/dist/packages/agent-pi/src/task-tools.js +53 -0
- package/dist/packages/agent-pi/src/types.js +1 -0
- package/dist/packages/agent-pi/src/usage.js +30 -0
- package/dist/packages/agent-pi/src/worker.js +602 -0
- package/dist/packages/protocol/src/activity-summary.js +77 -0
- package/dist/packages/protocol/src/agents.js +301 -0
- package/dist/packages/protocol/src/browser-access.js +1 -0
- package/dist/packages/protocol/src/checks.js +40 -0
- package/dist/packages/protocol/src/child-sessions.js +28 -0
- package/dist/packages/protocol/src/command-policy.js +3 -0
- package/dist/packages/protocol/src/coordinator.js +135 -0
- package/dist/packages/protocol/src/delegation.js +40 -0
- package/dist/packages/protocol/src/git-diff.js +71 -0
- package/dist/packages/protocol/src/git-handoff.js +1 -0
- package/dist/packages/protocol/src/index.js +1 -0
- package/dist/packages/protocol/src/memory.js +46 -0
- package/dist/packages/protocol/src/metrics.js +70 -0
- package/dist/packages/protocol/src/planning.js +138 -0
- package/dist/packages/protocol/src/retrieval.js +36 -0
- package/dist/packages/protocol/src/reviewers.js +17 -0
- package/dist/packages/protocol/src/search.js +16 -0
- package/dist/packages/protocol/src/session-changes.js +29 -0
- package/dist/packages/protocol/src/session-router.js +123 -0
- package/dist/packages/protocol/src/shell-evaluator.js +43 -0
- package/dist/packages/protocol/src/task-graph.js +1 -0
- package/dist/packages/protocol/src/token-usage.js +17 -0
- package/dist/packages/protocol/src/user-questions.js +106 -0
- package/package.json +71 -0
- package/vendor/lancedb/NODEJS_THIRD_PARTY_LICENSES.md +668 -0
- package/vendor/lancedb/RUST_THIRD_PARTY_LICENSES.html +14607 -0
- package/vendor/lancedb/dist/arrow.d.ts +296 -0
- package/vendor/lancedb/dist/arrow.js +1145 -0
- package/vendor/lancedb/dist/arrow_type.d.ts +9 -0
- package/vendor/lancedb/dist/arrow_type.js +29 -0
- package/vendor/lancedb/dist/connection.d.ts +508 -0
- package/vendor/lancedb/dist/connection.js +298 -0
- package/vendor/lancedb/dist/embedding/embedding_function.d.ts +103 -0
- package/vendor/lancedb/dist/embedding/embedding_function.js +192 -0
- package/vendor/lancedb/dist/embedding/index.d.ts +37 -0
- package/vendor/lancedb/dist/embedding/index.js +108 -0
- package/vendor/lancedb/dist/embedding/openai.d.ts +16 -0
- package/vendor/lancedb/dist/embedding/openai.js +81 -0
- package/vendor/lancedb/dist/embedding/registry.d.ts +94 -0
- package/vendor/lancedb/dist/embedding/registry.js +221 -0
- package/vendor/lancedb/dist/embedding/transformers.d.ts +36 -0
- package/vendor/lancedb/dist/embedding/transformers.js +110 -0
- package/vendor/lancedb/dist/header.d.ts +162 -0
- package/vendor/lancedb/dist/header.js +217 -0
- package/vendor/lancedb/dist/index.d.ts +246 -0
- package/vendor/lancedb/dist/index.js +176 -0
- package/vendor/lancedb/dist/indices.d.ts +721 -0
- package/vendor/lancedb/dist/indices.js +166 -0
- package/vendor/lancedb/dist/materialized_view.d.ts +69 -0
- package/vendor/lancedb/dist/materialized_view.js +112 -0
- package/vendor/lancedb/dist/merge.d.ts +104 -0
- package/vendor/lancedb/dist/merge.js +120 -0
- package/vendor/lancedb/dist/native.d.ts +1193 -0
- package/vendor/lancedb/dist/native.js +688 -0
- package/vendor/lancedb/dist/oauth.d.ts +66 -0
- package/vendor/lancedb/dist/oauth.js +15 -0
- package/vendor/lancedb/dist/otel.d.ts +26 -0
- package/vendor/lancedb/dist/otel.js +114 -0
- package/vendor/lancedb/dist/permutation.d.ts +143 -0
- package/vendor/lancedb/dist/permutation.js +184 -0
- package/vendor/lancedb/dist/query.d.ts +663 -0
- package/vendor/lancedb/dist/query.js +966 -0
- package/vendor/lancedb/dist/rerankers/index.d.ts +5 -0
- package/vendor/lancedb/dist/rerankers/index.js +19 -0
- package/vendor/lancedb/dist/rerankers/rrf.d.ts +14 -0
- package/vendor/lancedb/dist/rerankers/rrf.js +28 -0
- package/vendor/lancedb/dist/sanitize.d.ts +32 -0
- package/vendor/lancedb/dist/sanitize.js +544 -0
- package/vendor/lancedb/dist/scannable.d.ts +92 -0
- package/vendor/lancedb/dist/scannable.js +200 -0
- package/vendor/lancedb/dist/schema.d.ts +16 -0
- package/vendor/lancedb/dist/schema.js +387 -0
- package/vendor/lancedb/dist/table.d.ts +1069 -0
- package/vendor/lancedb/dist/table.js +498 -0
- package/vendor/lancedb/dist/util.d.ts +14 -0
- package/vendor/lancedb/dist/util.js +77 -0
- package/vendor/lancedb/license_header.txt +2 -0
- package/vendor/lancedb/package.json +56 -0
- package/vendor/transformers/LICENSE +202 -0
- package/vendor/transformers/dist/transformers.cjs +30844 -0
- package/vendor/transformers/dist/transformers.mjs +31404 -0
- package/vendor/transformers/package.json +55 -0
- package/vendor/transformers/types/backends/onnx.d.ts +30 -0
- package/vendor/transformers/types/configs.d.ts +85 -0
- package/vendor/transformers/types/env.d.ts +110 -0
- package/vendor/transformers/types/generation/configuration_utils.d.ts +320 -0
- package/vendor/transformers/types/generation/logits_process.d.ts +354 -0
- package/vendor/transformers/types/generation/logits_sampler.d.ts +51 -0
- package/vendor/transformers/types/generation/parameters.d.ts +47 -0
- package/vendor/transformers/types/generation/stopping_criteria.d.ts +81 -0
- package/vendor/transformers/types/generation/streamers.d.ts +81 -0
- package/vendor/transformers/types/models/whisper/common_whisper.d.ts +8 -0
- package/vendor/transformers/types/models/whisper/generation_whisper.d.ts +76 -0
- package/vendor/transformers/types/models.d.ts +3684 -0
- package/vendor/transformers/types/ops/registry.d.ts +11 -0
- package/vendor/transformers/types/pipelines.d.ts +2402 -0
- package/vendor/transformers/types/processors.d.ts +924 -0
- package/vendor/transformers/types/tokenizers.d.ts +990 -0
- package/vendor/transformers/types/transformers.d.ts +13 -0
- package/vendor/transformers/types/utils/audio.d.ts +130 -0
- package/vendor/transformers/types/utils/constants.d.ts +2 -0
- package/vendor/transformers/types/utils/core.d.ts +98 -0
- package/vendor/transformers/types/utils/data-structures.d.ts +236 -0
- package/vendor/transformers/types/utils/devices.d.ts +18 -0
- package/vendor/transformers/types/utils/dtypes.d.ts +19 -0
- package/vendor/transformers/types/utils/generic.d.ts +11 -0
- package/vendor/transformers/types/utils/hub.d.ts +152 -0
- package/vendor/transformers/types/utils/image.d.ts +119 -0
- package/vendor/transformers/types/utils/maths.d.ts +280 -0
- package/vendor/transformers/types/utils/tensor.d.ts +418 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { PiAgent } from '../../../packages/agent-pi/src/index.js';
|
|
2
|
+
import { isPiEvent } from '../../../packages/agent-pi/src/ipc.js';
|
|
3
|
+
import { projectAgentTools } from '../../../packages/agent-pi/src/agent-capabilities.js';
|
|
4
|
+
import { validateFinding } from '../../../packages/agent-pi/src/review.js';
|
|
5
|
+
import { agentId, parseAgentCapabilities, parseAgentDefinition, } from '../../../packages/protocol/src/agents.js';
|
|
6
|
+
import { parseUserQuestionRequest, parseUserResponse, } from '../../../packages/protocol/src/user-questions.js';
|
|
7
|
+
/** Explicit builder tests only. No general placements, hooks, publication or session mutation. */
|
|
8
|
+
export class AgentBuilder {
|
|
9
|
+
agents;
|
|
10
|
+
store;
|
|
11
|
+
capacity;
|
|
12
|
+
host;
|
|
13
|
+
factory;
|
|
14
|
+
workers = new Map();
|
|
15
|
+
owners = new Map();
|
|
16
|
+
failures = new Map();
|
|
17
|
+
disposals = new Set();
|
|
18
|
+
fail(id, owner, worker, error) {
|
|
19
|
+
if (this.failures.has(id))
|
|
20
|
+
return;
|
|
21
|
+
this.failures.set(id, String(error).slice(0, 4000));
|
|
22
|
+
const disposal = Promise.resolve(worker.dispose()).then(() => this.terminal(id, owner, 'failed', this.failures.get(id)));
|
|
23
|
+
this.disposals.add(disposal);
|
|
24
|
+
void disposal.finally(() => this.disposals.delete(disposal));
|
|
25
|
+
}
|
|
26
|
+
stopping = false;
|
|
27
|
+
constructor(agents, store, capacity, host, factory = (cb) => new PiAgent(cb)) {
|
|
28
|
+
this.agents = agents;
|
|
29
|
+
this.store = store;
|
|
30
|
+
this.capacity = capacity;
|
|
31
|
+
this.host = host;
|
|
32
|
+
this.factory = factory;
|
|
33
|
+
}
|
|
34
|
+
key(id) {
|
|
35
|
+
return `agent-test:${this.agents.projectId}:${id}`;
|
|
36
|
+
}
|
|
37
|
+
list() {
|
|
38
|
+
return this.agents
|
|
39
|
+
.list(100)
|
|
40
|
+
.filter((record) => Boolean(this.store.db.prepare('SELECT 1 FROM records WHERE key=?').get(this.key(record.id))));
|
|
41
|
+
}
|
|
42
|
+
read(id) {
|
|
43
|
+
const invocation = this.agents.read(id);
|
|
44
|
+
const saved = this.store.get(this.key(id));
|
|
45
|
+
if (!saved)
|
|
46
|
+
throw new Error('Builder test not found.');
|
|
47
|
+
if (!['running', 'prepared'].includes(invocation.status))
|
|
48
|
+
saved.questions = saved.questions.map((q) => q.status === 'pending' ? { ...q, status: 'cancelled' } : q);
|
|
49
|
+
return { invocation, ...saved };
|
|
50
|
+
}
|
|
51
|
+
start(request) {
|
|
52
|
+
if (this.stopping)
|
|
53
|
+
throw new Error('Server is stopping.');
|
|
54
|
+
agentId(request.requestId);
|
|
55
|
+
const definition = parseAgentDefinition(request.definition);
|
|
56
|
+
const permissions = parseAgentCapabilities(request.permissions);
|
|
57
|
+
if (permissions.some((c) => !definition.requestedCapabilities.includes(c)))
|
|
58
|
+
throw new Error('Test permissions must narrow requested capabilities.');
|
|
59
|
+
const fingerprint = JSON.stringify({ definition, input: request.input, permissions });
|
|
60
|
+
const receiptKey = `agent-test-receipt:${this.agents.projectId}:${request.requestId}`;
|
|
61
|
+
const receipt = this.store.get(receiptKey);
|
|
62
|
+
if (receipt) {
|
|
63
|
+
if (receipt.fingerprint !== fingerprint)
|
|
64
|
+
throw new Error('Test request ID was already used for different input.');
|
|
65
|
+
return this.read(receipt.id); // never replay, including after restart
|
|
66
|
+
}
|
|
67
|
+
const owner = `agent-builder:${this.agents.projectId}:${request.requestId}`;
|
|
68
|
+
if (!this.capacity.acquire(owner, `${this.store.directory}:main`))
|
|
69
|
+
throw new Error('Builder test execution capacity reached. Stop a test or wait.');
|
|
70
|
+
let record;
|
|
71
|
+
try {
|
|
72
|
+
record = this.agents.prepareTest(definition, request.input, permissions);
|
|
73
|
+
this.store.put(this.key(record.id), { events: [], questions: [] });
|
|
74
|
+
this.store.put(receiptKey, { fingerprint, id: record.id });
|
|
75
|
+
record = this.agents.claim(record.id, record.revision, permissions);
|
|
76
|
+
const id = record.id;
|
|
77
|
+
const worker = this.factory((event) => {
|
|
78
|
+
if (!isPiEvent(event) || event.runId !== record.runId || !this.workers.has(id))
|
|
79
|
+
return;
|
|
80
|
+
if (this.failures.has(id))
|
|
81
|
+
return;
|
|
82
|
+
try {
|
|
83
|
+
this.event(id, event);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
// Real Pi terminal events arrive only after worker exit; synthetic failures wait for disposal.
|
|
87
|
+
this.fail(id, owner, worker, error);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (event.type === 'complete' || event.type === 'error')
|
|
91
|
+
this.terminal(id, owner, event.type === 'error' ? 'failed' : event.cancelled ? 'cancelled' : 'completed', event.type === 'error' ? event.message : undefined);
|
|
92
|
+
});
|
|
93
|
+
this.workers.set(id, worker);
|
|
94
|
+
this.owners.set(id, owner);
|
|
95
|
+
worker.start(this.agents.startOptions(id, this.host()));
|
|
96
|
+
return this.read(id);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (record) {
|
|
100
|
+
const id = record.id;
|
|
101
|
+
const worker = this.workers.get(id);
|
|
102
|
+
if (worker)
|
|
103
|
+
this.fail(id, owner, worker, error);
|
|
104
|
+
else {
|
|
105
|
+
const current = this.agents.read(id);
|
|
106
|
+
if (current.status === 'prepared')
|
|
107
|
+
this.agents.cancel(id, current.revision);
|
|
108
|
+
else
|
|
109
|
+
this.terminal(id, owner, 'failed', String(error));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!record || !this.workers.has(record.id))
|
|
113
|
+
this.capacity.release(owner);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
event(id, event) {
|
|
118
|
+
const view = this.read(id), caps = view.invocation.effectiveCapabilities;
|
|
119
|
+
if (!['running', 'cancelling'].includes(view.invocation.status))
|
|
120
|
+
return;
|
|
121
|
+
if (event.type === 'tool_start' || event.type === 'tool_update' || event.type === 'tool_end') {
|
|
122
|
+
if (!projectAgentTools([{ name: event.name }], caps).length)
|
|
123
|
+
throw new Error(`Unauthorized builder test tool: ${event.name}.`);
|
|
124
|
+
}
|
|
125
|
+
if (event.type === 'finding') {
|
|
126
|
+
if (!caps.includes('findings.report') || !view.invocation.input.snapshot)
|
|
127
|
+
throw new Error('Unauthorized findings event.');
|
|
128
|
+
validateFinding(view.invocation.input.snapshot, event.finding);
|
|
129
|
+
}
|
|
130
|
+
else if (event.type === 'user_question') {
|
|
131
|
+
if (!caps.includes('user.ask'))
|
|
132
|
+
throw new Error('Unauthorized question event.');
|
|
133
|
+
if (view.invocation.status === 'cancelling')
|
|
134
|
+
return;
|
|
135
|
+
if (view.questions.some((q) => q.id === event.requestId))
|
|
136
|
+
throw new Error('Duplicate question.');
|
|
137
|
+
view.questions.push({
|
|
138
|
+
...parseUserQuestionRequest(event.question),
|
|
139
|
+
id: event.requestId,
|
|
140
|
+
runId: event.runId,
|
|
141
|
+
seq: view.events.length + 1,
|
|
142
|
+
status: 'pending',
|
|
143
|
+
version: 1,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
else if (![
|
|
147
|
+
'text',
|
|
148
|
+
'reasoning',
|
|
149
|
+
'tool_start',
|
|
150
|
+
'tool_update',
|
|
151
|
+
'tool_end',
|
|
152
|
+
'usage',
|
|
153
|
+
'complete',
|
|
154
|
+
'error',
|
|
155
|
+
].includes(event.type)) {
|
|
156
|
+
throw new Error(`Unsupported builder test event: ${event.type}.`);
|
|
157
|
+
}
|
|
158
|
+
if (view.events.length >= 4000 ||
|
|
159
|
+
JSON.stringify(view.events).length + JSON.stringify(event).length > 2_000_000)
|
|
160
|
+
throw new Error('Builder test transcript limit reached.');
|
|
161
|
+
view.events.push(event);
|
|
162
|
+
this.store.put(this.key(id), { events: view.events, questions: view.questions });
|
|
163
|
+
}
|
|
164
|
+
terminal(id, owner, outcome, error) {
|
|
165
|
+
const current = this.agents.read(id);
|
|
166
|
+
if (['running', 'cancelling'].includes(current.status))
|
|
167
|
+
this.agents.finish(id, current.revision, current.runId, outcome, error === undefined ? undefined : error.slice(0, 4000) || 'Agent test failed.');
|
|
168
|
+
this.workers.delete(id);
|
|
169
|
+
this.owners.delete(id);
|
|
170
|
+
this.failures.delete(id);
|
|
171
|
+
this.capacity.release(owner);
|
|
172
|
+
}
|
|
173
|
+
stop(id, revision) {
|
|
174
|
+
this.read(id);
|
|
175
|
+
this.agents.cancel(id, revision);
|
|
176
|
+
this.workers.get(id)?.cancel();
|
|
177
|
+
return this.read(id);
|
|
178
|
+
}
|
|
179
|
+
answer(id, questionId, version, value) {
|
|
180
|
+
const view = this.read(id), worker = this.workers.get(id);
|
|
181
|
+
const question = view.questions.find((q) => q.id === questionId);
|
|
182
|
+
if (!worker ||
|
|
183
|
+
view.invocation.status !== 'running' ||
|
|
184
|
+
!question ||
|
|
185
|
+
question.status !== 'pending' ||
|
|
186
|
+
question.version !== version)
|
|
187
|
+
throw new Error('Question changed or test is no longer running.');
|
|
188
|
+
question.answer = parseUserResponse(value, question);
|
|
189
|
+
question.status = 'answered';
|
|
190
|
+
question.version++;
|
|
191
|
+
this.store.put(this.key(id), { events: view.events, questions: view.questions });
|
|
192
|
+
worker.provideUserAnswer(questionId, question.answer);
|
|
193
|
+
return this.read(id);
|
|
194
|
+
}
|
|
195
|
+
async close() {
|
|
196
|
+
this.stopping = true;
|
|
197
|
+
await Promise.all([...this.workers.values()].map((w) => w.dispose()));
|
|
198
|
+
await Promise.all(this.disposals);
|
|
199
|
+
for (const owner of this.owners.values())
|
|
200
|
+
this.capacity.release(owner);
|
|
201
|
+
this.owners.clear();
|
|
202
|
+
this.workers.clear();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createConnection } from 'node:net';
|
|
2
|
+
import { once } from 'node:events';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
5
|
+
export function validateAttach(input) {
|
|
6
|
+
if (![input.project, input.session].every((id) => typeof id === 'string' && /^[a-z0-9-]{1,128}$/i.test(id)) ||
|
|
7
|
+
!Number.isSafeInteger(input.after) ||
|
|
8
|
+
input.after < 0)
|
|
9
|
+
throw new Error('Invalid attachment request.');
|
|
10
|
+
}
|
|
11
|
+
/** Private, read-only IPC with bounded pages and socket backpressure. */
|
|
12
|
+
export function streamAttachment(socket, input, read) {
|
|
13
|
+
validateAttach(input);
|
|
14
|
+
let after = input.after;
|
|
15
|
+
let timer;
|
|
16
|
+
const pump = () => {
|
|
17
|
+
if (socket.destroyed)
|
|
18
|
+
return;
|
|
19
|
+
try {
|
|
20
|
+
const events = read({ ...input, after });
|
|
21
|
+
if (socket.writableLength > 4 * 1024 * 1024) {
|
|
22
|
+
socket.destroy();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
for (const event of events) {
|
|
26
|
+
if (!socket.write(JSON.stringify({ type: 'event', event }) + '\n')) {
|
|
27
|
+
after = event.seq;
|
|
28
|
+
socket.once('drain', pump);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
after = event.seq;
|
|
32
|
+
}
|
|
33
|
+
timer = setTimeout(pump, events.length === 1000 ? 0 : 250);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
socket.end(JSON.stringify({ type: 'error', error: error.message }) + '\n');
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
// Validate the target before reporting a successful connection.
|
|
40
|
+
read(input);
|
|
41
|
+
socket.setTimeout(0);
|
|
42
|
+
socket.write(JSON.stringify({ type: 'ready' }) + '\n');
|
|
43
|
+
socket.once('close', () => {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
socket.removeListener('drain', pump);
|
|
46
|
+
});
|
|
47
|
+
pump();
|
|
48
|
+
}
|
|
49
|
+
export function attachOptions(args) {
|
|
50
|
+
const target = args.shift();
|
|
51
|
+
if (!target || target.startsWith('--'))
|
|
52
|
+
throw new Error('Usage: presubmit attach <session-id|session-url> [--project id] [--json]');
|
|
53
|
+
let session = target, project = 'main', after = 0, json = false;
|
|
54
|
+
if (/^https?:\/\//.test(target)) {
|
|
55
|
+
const url = new URL(target);
|
|
56
|
+
const match = url.pathname.match(/^\/s\/([^/]+)\/?$/);
|
|
57
|
+
if (!match)
|
|
58
|
+
throw new Error('Expected a session URL containing /s/<id>.');
|
|
59
|
+
session = decodeURIComponent(match[1]);
|
|
60
|
+
project = url.searchParams.get('project') ?? 'main';
|
|
61
|
+
}
|
|
62
|
+
while (args.length) {
|
|
63
|
+
const flag = args.shift();
|
|
64
|
+
if (flag === '--json')
|
|
65
|
+
json = true;
|
|
66
|
+
else if (flag === '--project')
|
|
67
|
+
project = args.shift() ?? '';
|
|
68
|
+
else if (flag === '--after') {
|
|
69
|
+
const value = args.shift();
|
|
70
|
+
if (!value || !/^\d+$/.test(value))
|
|
71
|
+
throw new Error('Invalid event cursor.');
|
|
72
|
+
after = Number(value);
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
throw new Error('Unknown attach option ' + flag);
|
|
76
|
+
}
|
|
77
|
+
validateAttach({ type: 'attach', session, project, after });
|
|
78
|
+
return { session, project, after, json };
|
|
79
|
+
}
|
|
80
|
+
export function renderActivity(event) {
|
|
81
|
+
const clean = (text) => stripVTControlCharacters(text).replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, '');
|
|
82
|
+
if (event.type === 'text')
|
|
83
|
+
return clean(event.text ?? '');
|
|
84
|
+
// Updates can be cumulative; show the final tool output once in plain-text mode.
|
|
85
|
+
if (event.type === 'tool_update' || event.type === 'workspace')
|
|
86
|
+
return '';
|
|
87
|
+
const label = event.type === 'approval' ? 'approval requested (check browser)' : event.type;
|
|
88
|
+
return `\n[${clean(label)}${event.toolName ? ': ' + clean(event.toolName) : ''}]${event.text ? ' ' + clean(event.text) : ''}\n`;
|
|
89
|
+
}
|
|
90
|
+
export async function followAttachment(path, options, output, signal, notice) {
|
|
91
|
+
let after = options.after;
|
|
92
|
+
let connected = false;
|
|
93
|
+
let retrying = false;
|
|
94
|
+
while (!signal.aborted) {
|
|
95
|
+
const socket = createConnection(path);
|
|
96
|
+
const abort = () => socket.destroy();
|
|
97
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
98
|
+
socket.setEncoding('utf8');
|
|
99
|
+
socket.setTimeout(10000, () => socket.destroy(new Error('Attachment handshake timed out.')));
|
|
100
|
+
socket.on('connect', () => socket.write(JSON.stringify({
|
|
101
|
+
type: 'attach',
|
|
102
|
+
project: options.project,
|
|
103
|
+
session: options.session,
|
|
104
|
+
after,
|
|
105
|
+
}) + '\n'));
|
|
106
|
+
let fatal;
|
|
107
|
+
try {
|
|
108
|
+
let pending = '';
|
|
109
|
+
for await (const chunk of socket) {
|
|
110
|
+
pending += chunk;
|
|
111
|
+
let newline;
|
|
112
|
+
while ((newline = pending.indexOf('\n')) !== -1) {
|
|
113
|
+
if (signal.aborted)
|
|
114
|
+
return;
|
|
115
|
+
const line = pending.slice(0, newline);
|
|
116
|
+
pending = pending.slice(newline + 1);
|
|
117
|
+
const message = JSON.parse(line);
|
|
118
|
+
if (message.type === 'error' || message.error) {
|
|
119
|
+
fatal = new Error(message.error);
|
|
120
|
+
throw fatal;
|
|
121
|
+
}
|
|
122
|
+
if (message.type === 'ready') {
|
|
123
|
+
connected = true;
|
|
124
|
+
retrying = false;
|
|
125
|
+
socket.setTimeout(0);
|
|
126
|
+
}
|
|
127
|
+
else if (message.type === 'event') {
|
|
128
|
+
const event = message.event;
|
|
129
|
+
if (!Number.isSafeInteger(event.seq) || event.seq <= after)
|
|
130
|
+
continue;
|
|
131
|
+
const text = options.json ? JSON.stringify(event) + '\n' : renderActivity(event);
|
|
132
|
+
if (text && !output.write(text))
|
|
133
|
+
await once(output, 'drain', { signal });
|
|
134
|
+
after = event.seq;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (pending.length > 8 * 1024 * 1024)
|
|
138
|
+
throw new Error('Attachment frame is too large.');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (signal.aborted)
|
|
143
|
+
return;
|
|
144
|
+
if (fatal)
|
|
145
|
+
throw fatal;
|
|
146
|
+
if (!connected)
|
|
147
|
+
throw new Error('Could not attach. Start or restart the Presubmit host with this version. ' +
|
|
148
|
+
error.message);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
signal.removeEventListener('abort', abort);
|
|
152
|
+
socket.destroy();
|
|
153
|
+
}
|
|
154
|
+
if (!connected && !signal.aborted)
|
|
155
|
+
throw new Error('Host does not support attachment. Restart it with this version.');
|
|
156
|
+
if (!signal.aborted) {
|
|
157
|
+
if (!retrying)
|
|
158
|
+
notice('Connection lost; reconnecting…');
|
|
159
|
+
retrying = true;
|
|
160
|
+
await delay(1000, undefined, { signal }).catch(() => { });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const MAX_SIZE = 25 * 1024 * 1024;
|
|
5
|
+
export function attachmentIds(value) {
|
|
6
|
+
if (value === undefined)
|
|
7
|
+
return [];
|
|
8
|
+
if (!Array.isArray(value) ||
|
|
9
|
+
value.length > 10 ||
|
|
10
|
+
value.some((id) => typeof id !== 'string' ||
|
|
11
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id)))
|
|
12
|
+
throw new Error('Invalid attachments (maximum 10 files).');
|
|
13
|
+
return [...new Set(value)];
|
|
14
|
+
}
|
|
15
|
+
export async function saveAttachment(directory, input) {
|
|
16
|
+
if (typeof input.name !== 'string' ||
|
|
17
|
+
!input.name.trim() ||
|
|
18
|
+
input.name.length > 255 ||
|
|
19
|
+
typeof input.type !== 'string' ||
|
|
20
|
+
input.type.length > 120 ||
|
|
21
|
+
typeof input.data !== 'string' ||
|
|
22
|
+
input.data.length > Math.ceil(MAX_SIZE / 3) * 4 ||
|
|
23
|
+
input.data.length % 4 !== 0)
|
|
24
|
+
throw new Error('Invalid attachment. Files must be at most 25 MB.');
|
|
25
|
+
const data = Buffer.from(input.data, 'base64');
|
|
26
|
+
if (data.toString('base64') !== input.data)
|
|
27
|
+
throw new Error('Invalid attachment encoding.');
|
|
28
|
+
if (data.length > MAX_SIZE)
|
|
29
|
+
throw new Error('Files must be at most 25 MB.');
|
|
30
|
+
const record = { id: randomUUID(), name: input.name, type: input.type, size: data.length };
|
|
31
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
32
|
+
await writeFile(join(directory, record.id), data, { flag: 'wx', mode: 0o600 });
|
|
33
|
+
await writeFile(join(directory, record.id + '.json'), JSON.stringify(record), {
|
|
34
|
+
flag: 'wx',
|
|
35
|
+
mode: 0o600,
|
|
36
|
+
});
|
|
37
|
+
return record;
|
|
38
|
+
}
|
|
39
|
+
export async function readAttachment(directory, id) {
|
|
40
|
+
attachmentIds([id]);
|
|
41
|
+
const record = JSON.parse(await readFile(join(directory, id + '.json'), 'utf8'));
|
|
42
|
+
return { ...record, data: (await readFile(join(directory, id))).toString('base64') };
|
|
43
|
+
}
|
|
44
|
+
export async function prepareAttachments(directory, ids) {
|
|
45
|
+
const records = [];
|
|
46
|
+
const images = [];
|
|
47
|
+
const descriptions = [];
|
|
48
|
+
let total = 0;
|
|
49
|
+
for (const id of attachmentIds(ids)) {
|
|
50
|
+
let record;
|
|
51
|
+
try {
|
|
52
|
+
record = JSON.parse(await readFile(join(directory, id + '.json'), 'utf8'));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new Error('An attachment is missing. Remove it and attach the file again.');
|
|
56
|
+
}
|
|
57
|
+
records.push(record);
|
|
58
|
+
total += record.size;
|
|
59
|
+
if (total > 50 * 1024 * 1024)
|
|
60
|
+
throw new Error('Attachments must total at most 50 MB.');
|
|
61
|
+
const file = join(directory, id);
|
|
62
|
+
descriptions.push(JSON.stringify({ name: record.name, type: record.type, size: record.size, path: file }));
|
|
63
|
+
if (['image/png', 'image/jpeg', 'image/gif', 'image/webp'].includes(record.type)) {
|
|
64
|
+
images.push({
|
|
65
|
+
type: 'image',
|
|
66
|
+
data: (await readFile(file)).toString('base64'),
|
|
67
|
+
mimeType: record.type,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else if (record.size <= 128 * 1024) {
|
|
71
|
+
const data = await readFile(file);
|
|
72
|
+
if (!data.includes(0) &&
|
|
73
|
+
(record.type.startsWith('text/') ||
|
|
74
|
+
/\.(txt|md|csv|json|ya?ml|ts|tsx|js|py|log|html|css)$/i.test(record.name)))
|
|
75
|
+
descriptions.push(JSON.stringify({ name: record.name, content: data.toString('utf8') }));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
records,
|
|
80
|
+
images,
|
|
81
|
+
context: descriptions.length
|
|
82
|
+
? '\n\nAttached files (reference material; distinguish file contents from the user request). Images are included visually. Other files, including videos, are available at the listed local paths; use available tools as needed.\n' +
|
|
83
|
+
descriptions.join('\n')
|
|
84
|
+
: '',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
/** Accept one explicit HTTPS device origin, never a wildcard or a proxy-supplied origin. */
|
|
3
|
+
export function tailscaleOrigin(value) {
|
|
4
|
+
let url;
|
|
5
|
+
try {
|
|
6
|
+
url = new URL(value);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
throw new Error('Tailscale requires an HTTPS device URL such as https://machine.tailnet.ts.net');
|
|
10
|
+
}
|
|
11
|
+
if (url.protocol !== 'https:' ||
|
|
12
|
+
url.port === '0' ||
|
|
13
|
+
!/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.){2}ts\.net$/.test(url.hostname) ||
|
|
14
|
+
url.username ||
|
|
15
|
+
url.password ||
|
|
16
|
+
url.pathname !== '/' ||
|
|
17
|
+
url.search ||
|
|
18
|
+
url.hash)
|
|
19
|
+
throw new Error('Tailscale requires an HTTPS device origin on ts.net, without a path or query');
|
|
20
|
+
return url.origin;
|
|
21
|
+
}
|
|
22
|
+
function equal(a, b) {
|
|
23
|
+
const x = Buffer.from(a), y = Buffer.from(b);
|
|
24
|
+
return x.length === y.length && timingSafeEqual(x, y);
|
|
25
|
+
}
|
|
26
|
+
export class BrowserAccess {
|
|
27
|
+
localCookie;
|
|
28
|
+
remoteCookie;
|
|
29
|
+
now;
|
|
30
|
+
localOrigin = '';
|
|
31
|
+
name;
|
|
32
|
+
localToken = randomBytes(32).toString('hex');
|
|
33
|
+
localUsed = false;
|
|
34
|
+
remoteTokens = new Map();
|
|
35
|
+
remote;
|
|
36
|
+
constructor(workspace, localCookie, remoteCookie, remoteOrigin, now = Date.now) {
|
|
37
|
+
this.localCookie = localCookie;
|
|
38
|
+
this.remoteCookie = remoteCookie;
|
|
39
|
+
this.now = now;
|
|
40
|
+
this.name = 'presubmit_' + createHash('sha256').update(workspace).digest('hex').slice(0, 12);
|
|
41
|
+
if (remoteOrigin)
|
|
42
|
+
this.setRemoteOrigin(remoteOrigin);
|
|
43
|
+
}
|
|
44
|
+
get remoteOrigin() {
|
|
45
|
+
return this.remote;
|
|
46
|
+
}
|
|
47
|
+
setRemoteOrigin(value) {
|
|
48
|
+
const origin = tailscaleOrigin(value);
|
|
49
|
+
if (origin !== this.remote)
|
|
50
|
+
this.remoteTokens.clear();
|
|
51
|
+
this.remote = origin;
|
|
52
|
+
}
|
|
53
|
+
requestOrigin(req) {
|
|
54
|
+
// Serve preserves Host. Forwarded and identity headers never grant access.
|
|
55
|
+
// Funnel sets this header on public traffic; this integration is tailnet-only.
|
|
56
|
+
if (req.headers['tailscale-funnel-request'] !== undefined)
|
|
57
|
+
return;
|
|
58
|
+
if (this.localOrigin && req.headers.host === new URL(this.localOrigin).host)
|
|
59
|
+
return this.localOrigin;
|
|
60
|
+
if (this.remoteOrigin && req.headers.host === new URL(this.remoteOrigin).host)
|
|
61
|
+
return this.remoteOrigin;
|
|
62
|
+
}
|
|
63
|
+
validOrigin(req, required = false) {
|
|
64
|
+
const origin = this.requestOrigin(req);
|
|
65
|
+
return Boolean(origin && (req.headers.origin === origin || (!required && !req.headers.origin)));
|
|
66
|
+
}
|
|
67
|
+
credentials(req) {
|
|
68
|
+
const origin = this.requestOrigin(req);
|
|
69
|
+
if (!origin)
|
|
70
|
+
return;
|
|
71
|
+
return origin === this.remoteOrigin
|
|
72
|
+
? { name: this.name + '_remote', value: this.remoteCookie, secure: '; Secure' }
|
|
73
|
+
: { name: this.name, value: this.localCookie, secure: '' };
|
|
74
|
+
}
|
|
75
|
+
validAuth(req) {
|
|
76
|
+
const credentials = this.credentials(req);
|
|
77
|
+
if (!credentials)
|
|
78
|
+
return false;
|
|
79
|
+
return String(req.headers.cookie ?? '')
|
|
80
|
+
.split(';')
|
|
81
|
+
.some((item) => {
|
|
82
|
+
const [name, ...value] = item.trim().split('=');
|
|
83
|
+
return name === credentials.name && equal(value.join('='), credentials.value);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
cookieHeader(req) {
|
|
87
|
+
const credentials = this.credentials(req);
|
|
88
|
+
if (!credentials)
|
|
89
|
+
throw new Error('Invalid host');
|
|
90
|
+
return `${credentials.name}=${credentials.value}; HttpOnly; SameSite=Strict; Path=/${credentials.secure}`;
|
|
91
|
+
}
|
|
92
|
+
localUrl() {
|
|
93
|
+
return `${this.localOrigin}/?token=${this.localToken}`;
|
|
94
|
+
}
|
|
95
|
+
remoteUrl(projectId = 'main') {
|
|
96
|
+
return this.remoteLink(projectId).url;
|
|
97
|
+
}
|
|
98
|
+
remoteLink(projectId = 'main') {
|
|
99
|
+
if (!this.remoteOrigin)
|
|
100
|
+
throw new Error('Start Tailscale Serve in Settings → Devices to connect another device.');
|
|
101
|
+
this.prune();
|
|
102
|
+
// Bound unconsumed links issued by either the CLI or an authenticated browser.
|
|
103
|
+
if (this.remoteTokens.size >= 32)
|
|
104
|
+
this.remoteTokens.delete(this.remoteTokens.keys().next().value);
|
|
105
|
+
const token = randomBytes(32).toString('hex');
|
|
106
|
+
const expiresAt = this.now() + 10 * 60_000;
|
|
107
|
+
this.remoteTokens.set(token, expiresAt);
|
|
108
|
+
const url = new URL(this.remoteOrigin);
|
|
109
|
+
if (projectId !== 'main')
|
|
110
|
+
url.searchParams.set('project', projectId);
|
|
111
|
+
url.searchParams.set('token', token);
|
|
112
|
+
return { url: url.toString(), expiresAt };
|
|
113
|
+
}
|
|
114
|
+
consume(req, token) {
|
|
115
|
+
if (!this.validOrigin(req) || typeof token !== 'string')
|
|
116
|
+
return false;
|
|
117
|
+
if (this.requestOrigin(req) === this.remoteOrigin) {
|
|
118
|
+
this.prune();
|
|
119
|
+
return this.remoteTokens.delete(token);
|
|
120
|
+
}
|
|
121
|
+
if (this.localUsed || !equal(token, this.localToken))
|
|
122
|
+
return false;
|
|
123
|
+
this.localUsed = true;
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
prune() {
|
|
127
|
+
for (const [token, expires] of this.remoteTokens)
|
|
128
|
+
if (expires <= this.now())
|
|
129
|
+
this.remoteTokens.delete(token);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { open, realpath, stat } from 'node:fs/promises';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { automaticEnvironment } from '../../../packages/agent-pi/src/shell.js';
|
|
7
|
+
import { repositoryPath } from '../../../packages/agent-pi/src/file-tools.js';
|
|
8
|
+
const exec = promisify(execFile);
|
|
9
|
+
/** Bounded starting observations, never a snapshot or a claim about consumed bytes. */
|
|
10
|
+
export async function checkProvenance(cwd) {
|
|
11
|
+
const path = await realpath(cwd);
|
|
12
|
+
const identity = await stat(path);
|
|
13
|
+
if (!identity.isDirectory())
|
|
14
|
+
throw new Error('Checkout is not a directory');
|
|
15
|
+
const result = {
|
|
16
|
+
mode: 'mutable_workspace',
|
|
17
|
+
path,
|
|
18
|
+
pathIdentity: `${identity.dev}:${identity.ino}`,
|
|
19
|
+
omissions: [
|
|
20
|
+
'Files may change during capture or execution; no revision is verified.',
|
|
21
|
+
'Fingerprint includes Git status and at most 32 listed files / 1 MiB total / 64 KiB per file. Ignored files, excluded paths, links, dependencies and environment are not captured.',
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
const deadline = Date.now() + 2000;
|
|
25
|
+
const git = async (args) => (await exec('/usr/bin/git', [
|
|
26
|
+
'--no-optional-locks',
|
|
27
|
+
'-c',
|
|
28
|
+
'core.fsmonitor=false',
|
|
29
|
+
'-c',
|
|
30
|
+
'core.hooksPath=/dev/null',
|
|
31
|
+
...args,
|
|
32
|
+
], {
|
|
33
|
+
cwd: path,
|
|
34
|
+
timeout: Math.max(1, deadline - Date.now()),
|
|
35
|
+
maxBuffer: 65536,
|
|
36
|
+
env: { ...automaticEnvironment(process.env, path), GIT_OPTIONAL_LOCKS: '0' },
|
|
37
|
+
})).stdout;
|
|
38
|
+
try {
|
|
39
|
+
result.startingCommit = (await git(['rev-parse', 'HEAD'])).trim();
|
|
40
|
+
const status = await git([
|
|
41
|
+
'status',
|
|
42
|
+
'--porcelain=v1',
|
|
43
|
+
'-z',
|
|
44
|
+
'--untracked-files=normal',
|
|
45
|
+
'--ignore-submodules=all',
|
|
46
|
+
]);
|
|
47
|
+
const files = (await git(['ls-files', '-z', '--cached', '--others', '--exclude-standard']))
|
|
48
|
+
.split('\0')
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.sort();
|
|
51
|
+
const hash = createHash('sha256').update('bounded-workspace-v1\0').update(status);
|
|
52
|
+
let bytes = 0, captured = 0;
|
|
53
|
+
for (const name of [...new Set(files)].slice(0, 32)) {
|
|
54
|
+
if (bytes >= 1024 * 1024 || Date.now() >= deadline)
|
|
55
|
+
break;
|
|
56
|
+
let file;
|
|
57
|
+
try {
|
|
58
|
+
const full = await repositoryPath(path, name);
|
|
59
|
+
file = await open(full, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
60
|
+
const info = await file.stat();
|
|
61
|
+
if (!info.isFile() ||
|
|
62
|
+
info.nlink !== 1 ||
|
|
63
|
+
info.size > 65536 ||
|
|
64
|
+
bytes + info.size > 1024 * 1024)
|
|
65
|
+
continue;
|
|
66
|
+
const buffer = Buffer.alloc(65537);
|
|
67
|
+
let length = 0;
|
|
68
|
+
while (length < buffer.length) {
|
|
69
|
+
const read = await file.read(buffer, length, buffer.length - length, length);
|
|
70
|
+
if (!read.bytesRead)
|
|
71
|
+
break;
|
|
72
|
+
length += read.bytesRead;
|
|
73
|
+
}
|
|
74
|
+
const after = await file.stat();
|
|
75
|
+
if (length > 65536 || after.nlink !== 1 || (await realpath(full)) !== full)
|
|
76
|
+
continue;
|
|
77
|
+
hash.update(JSON.stringify([name, length])).update(buffer.subarray(0, length));
|
|
78
|
+
bytes += length;
|
|
79
|
+
captured++;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* Unsafe, changed, oversized or unavailable files are omissions. */
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await file?.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
result.fingerprint = hash.digest('hex');
|
|
89
|
+
result.omissions.push(`Captured ${captured} of ${files.length} listed entries (${bytes} content bytes); all other entries omitted.`);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
result.omissions.push('Git observations unavailable or exceeded the 64 KiB listing / two-second capture budget.');
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|