taphound 0.2.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +200 -0
- package/README.zh-CN.md +200 -0
- package/assets/brand/taphound-mark.svg +9 -0
- package/assets/skills/taphound-ai-journey/GUIDE.md +862 -0
- package/assets/skills/taphound-ai-journey/SKILL.md +377 -0
- package/assets/skills/taphound-ai-journey/prompts/analyze-project.md +526 -0
- package/assets/skills/taphound-ai-journey/prompts/check-completion.md +52 -0
- package/assets/skills/taphound-ai-journey/prompts/generate-step.md +109 -0
- package/assets/skills/taphound-ai-journey/schemas/observe-output.json +42 -0
- package/assets/skills/taphound-ai-journey/schemas/project-context.json +81 -0
- package/assets/skills/taphound-ai-journey/schemas/proposed-step-envelope.json +267 -0
- package/assets/skills/taphound-ai-journey/templates/project-context.example.json +49 -0
- package/dist/adapters/adb/adb-adapter.d.ts +23 -0
- package/dist/adapters/adb/adb-adapter.js +218 -0
- package/dist/adapters/android-cli/android-cli-adapter.d.ts +11 -0
- package/dist/adapters/android-cli/android-cli-adapter.js +64 -0
- package/dist/adapters/android-cli/layout-parser.d.ts +3 -0
- package/dist/adapters/android-cli/layout-parser.js +174 -0
- package/dist/adapters/clock/system-clock.d.ts +5 -0
- package/dist/adapters/clock/system-clock.js +13 -0
- package/dist/adapters/filesystem/artifact-store.d.ts +4 -0
- package/dist/adapters/filesystem/artifact-store.js +73 -0
- package/dist/adapters/filesystem/generation-meta-writer.d.ts +16 -0
- package/dist/adapters/filesystem/generation-meta-writer.js +34 -0
- package/dist/adapters/filesystem/generation-session-store.d.ts +68 -0
- package/dist/adapters/filesystem/generation-session-store.js +1603 -0
- package/dist/adapters/filesystem/journey-writer.d.ts +16 -0
- package/dist/adapters/filesystem/journey-writer.js +34 -0
- package/dist/adapters/filesystem/project-bound-file.d.ts +6 -0
- package/dist/adapters/filesystem/project-bound-file.js +240 -0
- package/dist/adapters/filesystem/project-file-inspector.d.ts +8 -0
- package/dist/adapters/filesystem/project-file-inspector.js +198 -0
- package/dist/adapters/filesystem/skill-installer.d.ts +10 -0
- package/dist/adapters/filesystem/skill-installer.js +39 -0
- package/dist/adapters/process/node-process-runner.d.ts +8 -0
- package/dist/adapters/process/node-process-runner.js +155 -0
- package/dist/adapters/prompt/inquirer-generation-prompt.d.ts +55 -0
- package/dist/adapters/prompt/inquirer-generation-prompt.js +213 -0
- package/dist/adapters/prompt/inquirer-init-prompt.d.ts +32 -0
- package/dist/adapters/prompt/inquirer-init-prompt.js +55 -0
- package/dist/adapters/prompt/inquirer-recorder-prompt.d.ts +47 -0
- package/dist/adapters/prompt/inquirer-recorder-prompt.js +157 -0
- package/dist/application/assertion/expectation-evaluator.d.ts +61 -0
- package/dist/application/assertion/expectation-evaluator.js +207 -0
- package/dist/application/collector/logcat-collector.d.ts +39 -0
- package/dist/application/collector/logcat-collector.js +107 -0
- package/dist/application/context/context-validator.d.ts +27 -0
- package/dist/application/context/context-validator.js +120 -0
- package/dist/application/doctor/doctor-service.d.ts +35 -0
- package/dist/application/doctor/doctor-service.js +178 -0
- package/dist/application/generation/focused-input.d.ts +2 -0
- package/dist/application/generation/focused-input.js +9 -0
- package/dist/application/generation/generation-confirmation-service.d.ts +76 -0
- package/dist/application/generation/generation-confirmation-service.js +383 -0
- package/dist/application/generation/generation-finalizer.d.ts +67 -0
- package/dist/application/generation/generation-finalizer.js +646 -0
- package/dist/application/generation/generation-publisher.d.ts +46 -0
- package/dist/application/generation/generation-publisher.js +178 -0
- package/dist/application/generation/generation-starter.d.ts +31 -0
- package/dist/application/generation/generation-starter.js +102 -0
- package/dist/application/generation/generation-step-executor.d.ts +52 -0
- package/dist/application/generation/generation-step-executor.js +668 -0
- package/dist/application/generation/proposed-step-validator.d.ts +11 -0
- package/dist/application/generation/proposed-step-validator.js +88 -0
- package/dist/application/generation/risk-evaluator.d.ts +9 -0
- package/dist/application/generation/risk-evaluator.js +14 -0
- package/dist/application/generation/runtime-observer.d.ts +39 -0
- package/dist/application/generation/runtime-observer.js +157 -0
- package/dist/application/init/init-service.d.ts +17 -0
- package/dist/application/init/init-service.js +44 -0
- package/dist/application/interaction/action-executor.d.ts +25 -0
- package/dist/application/interaction/action-executor.js +103 -0
- package/dist/application/interaction/fallback-resolver.d.ts +24 -0
- package/dist/application/interaction/fallback-resolver.js +48 -0
- package/dist/application/interaction/scroll-to-executor.d.ts +40 -0
- package/dist/application/interaction/scroll-to-executor.js +191 -0
- package/dist/application/locator/locator-resolver.d.ts +20 -0
- package/dist/application/locator/locator-resolver.js +89 -0
- package/dist/application/project/project-describer.d.ts +14 -0
- package/dist/application/project/project-describer.js +8 -0
- package/dist/application/recorder/locator-selector.d.ts +10 -0
- package/dist/application/recorder/locator-selector.js +80 -0
- package/dist/application/recorder/recorder-service.d.ts +41 -0
- package/dist/application/recorder/recorder-service.js +331 -0
- package/dist/application/report/report-writer.d.ts +10 -0
- package/dist/application/report/report-writer.js +39 -0
- package/dist/application/runtime/activity-waiter.d.ts +33 -0
- package/dist/application/runtime/activity-waiter.js +73 -0
- package/dist/application/runtime/launch-failure.d.ts +2 -0
- package/dist/application/runtime/launch-failure.js +20 -0
- package/dist/application/runtime/process-waiter.d.ts +27 -0
- package/dist/application/runtime/process-waiter.js +73 -0
- package/dist/application/runtime/step-runner.d.ts +50 -0
- package/dist/application/runtime/step-runner.js +429 -0
- package/dist/application/runtime/verify-runtime.d.ts +44 -0
- package/dist/application/runtime/verify-runtime.js +350 -0
- package/dist/application/wait/idle-waiter.d.ts +30 -0
- package/dist/application/wait/idle-waiter.js +90 -0
- package/dist/cli/commands/context.d.ts +3 -0
- package/dist/cli/commands/context.js +94 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +51 -0
- package/dist/cli/commands/generation.d.ts +3 -0
- package/dist/cli/commands/generation.js +518 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.js +104 -0
- package/dist/cli/commands/project.d.ts +3 -0
- package/dist/cli/commands/project.js +58 -0
- package/dist/cli/commands/record.d.ts +3 -0
- package/dist/cli/commands/record.js +88 -0
- package/dist/cli/commands/verify.d.ts +3 -0
- package/dist/cli/commands/verify.js +106 -0
- package/dist/cli/dependencies.d.ts +66 -0
- package/dist/cli/dependencies.js +204 -0
- package/dist/cli/main.d.ts +8 -0
- package/dist/cli/main.js +73 -0
- package/dist/cli/output.d.ts +16 -0
- package/dist/cli/output.js +22 -0
- package/dist/cli/program.d.ts +3 -0
- package/dist/cli/program.js +43 -0
- package/dist/domain/activity.d.ts +7 -0
- package/dist/domain/activity.js +65 -0
- package/dist/domain/app-process.d.ts +7 -0
- package/dist/domain/app-process.js +10 -0
- package/dist/domain/config.d.ts +15 -0
- package/dist/domain/config.js +17 -0
- package/dist/domain/failure.d.ts +4 -0
- package/dist/domain/failure.js +43 -0
- package/dist/domain/generation.d.ts +912 -0
- package/dist/domain/generation.js +342 -0
- package/dist/domain/init.d.ts +42 -0
- package/dist/domain/init.js +114 -0
- package/dist/domain/journey.d.ts +673 -0
- package/dist/domain/journey.js +109 -0
- package/dist/domain/layout.d.ts +35 -0
- package/dist/domain/layout.js +33 -0
- package/dist/domain/locator.d.ts +8 -0
- package/dist/domain/locator.js +5 -0
- package/dist/domain/project-context.d.ts +101 -0
- package/dist/domain/project-context.js +64 -0
- package/dist/domain/proposed-step.d.ts +341 -0
- package/dist/domain/proposed-step.js +87 -0
- package/dist/domain/report.d.ts +349 -0
- package/dist/domain/report.js +135 -0
- package/dist/domain/runtime-snapshot.d.ts +16 -0
- package/dist/domain/runtime-snapshot.js +46 -0
- package/dist/ports/adb.d.ts +42 -0
- package/dist/ports/adb.js +1 -0
- package/dist/ports/android-cli.d.ts +21 -0
- package/dist/ports/android-cli.js +1 -0
- package/dist/ports/artifact-store.d.ts +12 -0
- package/dist/ports/artifact-store.js +1 -0
- package/dist/ports/clock.d.ts +4 -0
- package/dist/ports/clock.js +1 -0
- package/dist/ports/generation-meta-writer.d.ts +10 -0
- package/dist/ports/generation-meta-writer.js +1 -0
- package/dist/ports/generation-prompt.d.ts +19 -0
- package/dist/ports/generation-prompt.js +6 -0
- package/dist/ports/generation-session-store.d.ts +32 -0
- package/dist/ports/generation-session-store.js +26 -0
- package/dist/ports/init-prompt.d.ts +8 -0
- package/dist/ports/init-prompt.js +6 -0
- package/dist/ports/journey-writer.d.ts +10 -0
- package/dist/ports/journey-writer.js +1 -0
- package/dist/ports/process-runner.d.ts +32 -0
- package/dist/ports/process-runner.js +1 -0
- package/dist/ports/project-bound-file.d.ts +8 -0
- package/dist/ports/project-bound-file.js +1 -0
- package/dist/ports/project-file-inspector.d.ts +33 -0
- package/dist/ports/project-file-inspector.js +1 -0
- package/dist/ports/recorder-prompt.d.ts +29 -0
- package/dist/ports/recorder-prompt.js +1 -0
- package/dist/ports/skill-installer.d.ts +8 -0
- package/dist/ports/skill-installer.js +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { GenerationConfirmationEvidenceSchema, GenerationSessionIdSchema, GenerationSessionSchema, hashGenerationConfirmationEvidence } from "../../domain/generation.js";
|
|
2
|
+
import { ProposedStepSchema, hashProposedStep } from "../../domain/proposed-step.js";
|
|
3
|
+
import { RuntimeSnapshotSchema, hashRuntimeSnapshot } from "../../domain/runtime-snapshot.js";
|
|
4
|
+
import { GenerationPromptCancelledError } from "../../ports/generation-prompt.js";
|
|
5
|
+
import { GenerationOperationError } from "./generation-starter.js";
|
|
6
|
+
import { ProposedStepValidator } from "./proposed-step-validator.js";
|
|
7
|
+
import { RiskEvaluator } from "./risk-evaluator.js";
|
|
8
|
+
function locatorSummary(step) {
|
|
9
|
+
if (step.action === "inputText"
|
|
10
|
+
|| step.action === "back"
|
|
11
|
+
|| step.action === "wait") {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
return JSON.stringify(step.locator);
|
|
15
|
+
}
|
|
16
|
+
export function summarizeProposedStep(step) {
|
|
17
|
+
const before = step.activity.before;
|
|
18
|
+
if (step.action === "back") {
|
|
19
|
+
return `Back from ${before}`;
|
|
20
|
+
}
|
|
21
|
+
if (step.action === "wait") {
|
|
22
|
+
return `Wait on ${before}`;
|
|
23
|
+
}
|
|
24
|
+
if (step.action === "inputText") {
|
|
25
|
+
return `Input text on focused element in ${before}`;
|
|
26
|
+
}
|
|
27
|
+
if (step.action === "scrollTo") {
|
|
28
|
+
return `Scroll ${step.direction} in ${JSON.stringify(step.container)} to ${locatorSummary(step)} on ${before}`;
|
|
29
|
+
}
|
|
30
|
+
return `${step.action} ${locatorSummary(step)} on ${before}`;
|
|
31
|
+
}
|
|
32
|
+
function bindingFailure(message) {
|
|
33
|
+
return new GenerationOperationError("RISK_CONFIRMATION_REQUIRED", message);
|
|
34
|
+
}
|
|
35
|
+
export function confirmationEvidencePath(challengeId) {
|
|
36
|
+
return `evidence/confirmations/${GenerationSessionIdSchema.parse(challengeId)}.json`;
|
|
37
|
+
}
|
|
38
|
+
export class GenerationConfirmationService {
|
|
39
|
+
dependencies;
|
|
40
|
+
validator = new ProposedStepValidator();
|
|
41
|
+
riskEvaluator = new RiskEvaluator();
|
|
42
|
+
constructor(dependencies) {
|
|
43
|
+
this.dependencies = dependencies;
|
|
44
|
+
if (!Number.isSafeInteger(dependencies.confirmationTtlMs)
|
|
45
|
+
|| dependencies.confirmationTtlMs <= 0) {
|
|
46
|
+
throw new Error("Confirmation TTL must be a positive safe integer");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
request = async (input) => {
|
|
50
|
+
const session = GenerationSessionSchema.parse(await this.dependencies.store.read(input.generationId));
|
|
51
|
+
const submittedEvidence = GenerationConfirmationEvidenceSchema.parse({
|
|
52
|
+
version: 1,
|
|
53
|
+
proposal: input.proposal,
|
|
54
|
+
snapshot: input.snapshot,
|
|
55
|
+
source: input.source ?? "planner"
|
|
56
|
+
});
|
|
57
|
+
if (session.pendingConfirmation !== null
|
|
58
|
+
&& session.pendingConfirmation.status === "pending") {
|
|
59
|
+
const storedEvidence = await this.readStoredEvidence({
|
|
60
|
+
generationId: session.id,
|
|
61
|
+
challengeId: session.pendingConfirmation.challengeId
|
|
62
|
+
});
|
|
63
|
+
if (JSON.stringify(storedEvidence) !== JSON.stringify(submittedEvidence)) {
|
|
64
|
+
throw bindingFailure("Pending generation confirmation does not match submitted evidence");
|
|
65
|
+
}
|
|
66
|
+
const challenge = this.requireExactPendingChallenge(session, storedEvidence);
|
|
67
|
+
return { status: "confirmationRequired", challenge };
|
|
68
|
+
}
|
|
69
|
+
const proposal = this.validator.validate({
|
|
70
|
+
session,
|
|
71
|
+
snapshot: submittedEvidence.snapshot,
|
|
72
|
+
proposal: submittedEvidence.proposal
|
|
73
|
+
});
|
|
74
|
+
const risk = this.riskEvaluator.evaluate(proposal.action, session.target.interactionPolicy);
|
|
75
|
+
if (risk.effectiveRisk === "forbidden") {
|
|
76
|
+
throw new GenerationOperationError("ACTION_FORBIDDEN", `${proposal.action} is forbidden by the Core interaction policy`);
|
|
77
|
+
}
|
|
78
|
+
if (risk.effectiveRisk === "safe") {
|
|
79
|
+
return { status: "approved", proposal };
|
|
80
|
+
}
|
|
81
|
+
if (session.revision === Number.MAX_SAFE_INTEGER) {
|
|
82
|
+
throw bindingFailure("Generation revision cannot create a challenge");
|
|
83
|
+
}
|
|
84
|
+
const evidence = GenerationConfirmationEvidenceSchema.parse({
|
|
85
|
+
version: 1,
|
|
86
|
+
proposal,
|
|
87
|
+
snapshot: submittedEvidence.snapshot,
|
|
88
|
+
source: submittedEvidence.source
|
|
89
|
+
});
|
|
90
|
+
const challenge = {
|
|
91
|
+
challengeId: this.dependencies.generateChallengeId(),
|
|
92
|
+
stepIndex: session.candidateSteps.length,
|
|
93
|
+
proposalHash: hashProposedStep(proposal),
|
|
94
|
+
snapshotHash: proposal.binding.snapshotHash,
|
|
95
|
+
evidenceHash: hashGenerationConfirmationEvidence(evidence),
|
|
96
|
+
actionSummary: summarizeProposedStep(proposal),
|
|
97
|
+
expiresAt: new Date(this.dependencies.now().getTime()
|
|
98
|
+
+ this.dependencies.confirmationTtlMs).toISOString(),
|
|
99
|
+
status: "pending"
|
|
100
|
+
};
|
|
101
|
+
const next = GenerationSessionSchema.parse({
|
|
102
|
+
...session,
|
|
103
|
+
revision: session.revision + 1,
|
|
104
|
+
pendingConfirmation: challenge
|
|
105
|
+
});
|
|
106
|
+
await this.dependencies.store.writeEvidence(session.id, confirmationEvidencePath(challenge.challengeId), evidence);
|
|
107
|
+
await this.updateConfirmationReconciled(session.id, session.revision, next);
|
|
108
|
+
return { status: "confirmationRequired", challenge };
|
|
109
|
+
};
|
|
110
|
+
requestManual = async (input) => {
|
|
111
|
+
const proposal = await this.dependencies.prompt.buildManualProposal(input.manual, input.signal);
|
|
112
|
+
if (input.signal?.aborted === true) {
|
|
113
|
+
throw new GenerationPromptCancelledError();
|
|
114
|
+
}
|
|
115
|
+
return this.request({
|
|
116
|
+
generationId: input.generationId,
|
|
117
|
+
proposal,
|
|
118
|
+
snapshot: input.snapshot,
|
|
119
|
+
source: "manualOverride"
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
findPendingManual = async (input) => {
|
|
123
|
+
const session = GenerationSessionSchema.parse(await this.dependencies.store.read(input.generationId));
|
|
124
|
+
const pending = session.pendingConfirmation;
|
|
125
|
+
if (pending === null || pending.status !== "pending") {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const evidence = await this.readStoredEvidence({
|
|
129
|
+
generationId: session.id,
|
|
130
|
+
challengeId: pending.challengeId
|
|
131
|
+
});
|
|
132
|
+
if (evidence.source !== "manualOverride"
|
|
133
|
+
|| evidence.proposal.action !== input.action) {
|
|
134
|
+
throw bindingFailure("Pending manual confirmation does not match the requested action");
|
|
135
|
+
}
|
|
136
|
+
const challenge = this.requireExactPendingChallenge(session, evidence);
|
|
137
|
+
return { status: "confirmationRequired", challenge };
|
|
138
|
+
};
|
|
139
|
+
confirmStored = async (input) => {
|
|
140
|
+
const evidence = await this.readStoredEvidence(input);
|
|
141
|
+
await this.confirm({
|
|
142
|
+
generationId: input.generationId,
|
|
143
|
+
challengeId: input.challengeId,
|
|
144
|
+
proposal: evidence.proposal,
|
|
145
|
+
snapshot: evidence.snapshot,
|
|
146
|
+
source: evidence.source,
|
|
147
|
+
signal: input.signal
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
status: "approved",
|
|
151
|
+
proposal: evidence.proposal,
|
|
152
|
+
snapshot: evidence.snapshot,
|
|
153
|
+
source: evidence.source
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
confirm = async (input) => {
|
|
157
|
+
const session = GenerationSessionSchema.parse(await this.dependencies.store.read(input.generationId));
|
|
158
|
+
const proposal = ProposedStepSchema.parse(input.proposal);
|
|
159
|
+
const snapshot = RuntimeSnapshotSchema.parse(input.snapshot);
|
|
160
|
+
const evidence = GenerationConfirmationEvidenceSchema.parse({
|
|
161
|
+
version: 1,
|
|
162
|
+
proposal,
|
|
163
|
+
snapshot,
|
|
164
|
+
source: input.source ?? "planner"
|
|
165
|
+
});
|
|
166
|
+
const evidenceHash = hashGenerationConfirmationEvidence(evidence);
|
|
167
|
+
const challenge = session.pendingConfirmation;
|
|
168
|
+
if (challenge === null) {
|
|
169
|
+
throw bindingFailure("No pending generation confirmation exists");
|
|
170
|
+
}
|
|
171
|
+
const expectedRevision = proposal.binding.baseRevision
|
|
172
|
+
+ (challenge.status === "pending" ? 1 : 2);
|
|
173
|
+
const cleanupToken = {
|
|
174
|
+
expectedRevision,
|
|
175
|
+
expectedChallenge: challenge
|
|
176
|
+
};
|
|
177
|
+
const expired = this.dependencies.now().getTime()
|
|
178
|
+
>= new Date(challenge.expiresAt).getTime();
|
|
179
|
+
const challengeMatches = challenge.challengeId === input.challengeId;
|
|
180
|
+
const proposalMatches = challenge.proposalHash
|
|
181
|
+
=== hashProposedStep(proposal);
|
|
182
|
+
const evidenceMatches = challenge.evidenceHash === evidenceHash;
|
|
183
|
+
const snapshotMatches = challenge.snapshotHash
|
|
184
|
+
=== hashRuntimeSnapshot(snapshot)
|
|
185
|
+
&& challenge.snapshotHash === proposal.binding.snapshotHash
|
|
186
|
+
&& challenge.snapshotHash === session.bindings.snapshotHash;
|
|
187
|
+
const stateMatches = session.state === "active"
|
|
188
|
+
&& session.inFlight === null
|
|
189
|
+
&& session.verification.status === "notRun"
|
|
190
|
+
&& session.publication.status === "notRun"
|
|
191
|
+
&& session.revision === expectedRevision
|
|
192
|
+
&& challenge.stepIndex === session.candidateSteps.length
|
|
193
|
+
&& challenge.actionSummary === summarizeProposedStep(proposal);
|
|
194
|
+
if (expired
|
|
195
|
+
|| !challengeMatches
|
|
196
|
+
|| !proposalMatches
|
|
197
|
+
|| !evidenceMatches
|
|
198
|
+
|| !snapshotMatches
|
|
199
|
+
|| !stateMatches) {
|
|
200
|
+
if (challengeMatches) {
|
|
201
|
+
await this.clearExactChallenge(session.id, cleanupToken);
|
|
202
|
+
}
|
|
203
|
+
if (expired) {
|
|
204
|
+
throw bindingFailure("Generation confirmation challenge expired");
|
|
205
|
+
}
|
|
206
|
+
if (!evidenceMatches) {
|
|
207
|
+
throw bindingFailure("Generation confirmation evidence hash is not authoritative");
|
|
208
|
+
}
|
|
209
|
+
throw bindingFailure("Generation confirmation binding is no longer authoritative");
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const validationSession = GenerationSessionSchema.parse({
|
|
213
|
+
...session,
|
|
214
|
+
revision: proposal.binding.baseRevision,
|
|
215
|
+
pendingConfirmation: null
|
|
216
|
+
});
|
|
217
|
+
this.validator.validate({
|
|
218
|
+
session: validationSession,
|
|
219
|
+
snapshot,
|
|
220
|
+
proposal
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
await this.clearExactChallenge(session.id, cleanupToken);
|
|
225
|
+
throw bindingFailure("Generation proposal or runtime state changed before confirmation");
|
|
226
|
+
}
|
|
227
|
+
if (challenge.status === "approved") {
|
|
228
|
+
return { status: "approved", proposal };
|
|
229
|
+
}
|
|
230
|
+
let confirmed;
|
|
231
|
+
try {
|
|
232
|
+
confirmed = await this.dependencies.prompt.confirm(challenge, input.signal);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
await this.clearExactChallenge(session.id, cleanupToken);
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
if (input.signal?.aborted === true) {
|
|
239
|
+
await this.cancelPrompt(session.id, cleanupToken);
|
|
240
|
+
}
|
|
241
|
+
if (!confirmed) {
|
|
242
|
+
await this.clearExactChallenge(session.id, cleanupToken);
|
|
243
|
+
throw bindingFailure("Generation action confirmation was declined");
|
|
244
|
+
}
|
|
245
|
+
const latest = GenerationSessionSchema.parse(await this.dependencies.store.read(input.generationId));
|
|
246
|
+
if (input.signal?.aborted === true) {
|
|
247
|
+
await this.cancelPrompt(session.id, cleanupToken);
|
|
248
|
+
}
|
|
249
|
+
const stillAuthoritative = latest.revision === session.revision
|
|
250
|
+
&& JSON.stringify(latest.pendingConfirmation)
|
|
251
|
+
=== JSON.stringify(challenge)
|
|
252
|
+
&& this.dependencies.now().getTime()
|
|
253
|
+
< new Date(challenge.expiresAt).getTime();
|
|
254
|
+
if (!stillAuthoritative) {
|
|
255
|
+
await this.clearExactChallenge(session.id, cleanupToken);
|
|
256
|
+
throw bindingFailure("Generation confirmation state changed while prompting");
|
|
257
|
+
}
|
|
258
|
+
if (latest.revision === Number.MAX_SAFE_INTEGER) {
|
|
259
|
+
await this.clearExactChallenge(session.id, cleanupToken);
|
|
260
|
+
throw bindingFailure("Generation revision cannot approve challenge");
|
|
261
|
+
}
|
|
262
|
+
if (input.signal?.aborted === true) {
|
|
263
|
+
await this.cancelPrompt(session.id, cleanupToken);
|
|
264
|
+
}
|
|
265
|
+
const approved = GenerationSessionSchema.parse({
|
|
266
|
+
...latest,
|
|
267
|
+
revision: latest.revision + 1,
|
|
268
|
+
pendingConfirmation: {
|
|
269
|
+
...challenge,
|
|
270
|
+
status: "approved"
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
await this.dependencies.store.updateConfirmation(latest.id, latest.revision, approved);
|
|
274
|
+
return { status: "approved", proposal };
|
|
275
|
+
};
|
|
276
|
+
clearApproved = async (input) => {
|
|
277
|
+
const session = GenerationSessionSchema.parse(await this.dependencies.store.read(input.generationId));
|
|
278
|
+
const challenge = session.pendingConfirmation;
|
|
279
|
+
if (challenge === null
|
|
280
|
+
|| JSON.stringify(challenge) !== JSON.stringify(input.challenge)
|
|
281
|
+
|| challenge.status !== "approved") {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
await this.clearExactChallenge(session.id, {
|
|
285
|
+
expectedRevision: session.revision,
|
|
286
|
+
expectedChallenge: challenge
|
|
287
|
+
});
|
|
288
|
+
};
|
|
289
|
+
async clearExactChallenge(generationId, token) {
|
|
290
|
+
const latest = GenerationSessionSchema.parse(await this.dependencies.store.read(generationId));
|
|
291
|
+
const pending = latest.pendingConfirmation;
|
|
292
|
+
if (latest.revision !== token.expectedRevision
|
|
293
|
+
|| pending === null
|
|
294
|
+
|| JSON.stringify(pending) !== JSON.stringify(token.expectedChallenge)) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (latest.revision === Number.MAX_SAFE_INTEGER) {
|
|
298
|
+
throw bindingFailure("Generation revision cannot clear challenge");
|
|
299
|
+
}
|
|
300
|
+
const cleared = GenerationSessionSchema.parse({
|
|
301
|
+
...latest,
|
|
302
|
+
revision: latest.revision + 1,
|
|
303
|
+
pendingConfirmation: null
|
|
304
|
+
});
|
|
305
|
+
await this.updateConfirmationReconciled(latest.id, latest.revision, cleared);
|
|
306
|
+
}
|
|
307
|
+
async cancelPrompt(generationId, token) {
|
|
308
|
+
await this.clearExactChallenge(generationId, token);
|
|
309
|
+
throw new GenerationPromptCancelledError();
|
|
310
|
+
}
|
|
311
|
+
requireExactPendingChallenge(session, evidence) {
|
|
312
|
+
const challenge = session.pendingConfirmation;
|
|
313
|
+
const proposal = evidence.proposal;
|
|
314
|
+
const snapshot = evidence.snapshot;
|
|
315
|
+
if (challenge === null
|
|
316
|
+
|| challenge.status !== "pending"
|
|
317
|
+
|| session.revision !== proposal.binding.baseRevision + 1) {
|
|
318
|
+
throw bindingFailure("Pending generation confirmation is no longer authoritative");
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
const baseSession = GenerationSessionSchema.parse({
|
|
322
|
+
...session,
|
|
323
|
+
revision: proposal.binding.baseRevision,
|
|
324
|
+
pendingConfirmation: null
|
|
325
|
+
});
|
|
326
|
+
this.validator.validate({
|
|
327
|
+
session: baseSession,
|
|
328
|
+
snapshot,
|
|
329
|
+
proposal
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
throw bindingFailure("Pending generation confirmation is no longer authoritative");
|
|
334
|
+
}
|
|
335
|
+
const risk = this.riskEvaluator.evaluate(proposal.action, session.target.interactionPolicy);
|
|
336
|
+
if (risk.effectiveRisk !== "confirmationRequired"
|
|
337
|
+
|| challenge.stepIndex !== session.candidateSteps.length
|
|
338
|
+
|| challenge.proposalHash !== hashProposedStep(proposal)
|
|
339
|
+
|| challenge.snapshotHash !== hashRuntimeSnapshot(snapshot)
|
|
340
|
+
|| challenge.snapshotHash !== proposal.binding.snapshotHash
|
|
341
|
+
|| challenge.evidenceHash !== hashGenerationConfirmationEvidence(evidence)
|
|
342
|
+
|| challenge.actionSummary !== summarizeProposedStep(proposal)
|
|
343
|
+
|| this.dependencies.now().getTime()
|
|
344
|
+
>= new Date(challenge.expiresAt).getTime()) {
|
|
345
|
+
throw bindingFailure("Pending generation confirmation does not match stored evidence");
|
|
346
|
+
}
|
|
347
|
+
return challenge;
|
|
348
|
+
}
|
|
349
|
+
async updateConfirmationReconciled(generationId, expectedRevision, intended) {
|
|
350
|
+
try {
|
|
351
|
+
await this.dependencies.store.updateConfirmation(generationId, expectedRevision, intended);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
let authoritative;
|
|
355
|
+
try {
|
|
356
|
+
authoritative = GenerationSessionSchema.parse(await this.dependencies.store.read(generationId));
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
if (JSON.stringify(authoritative) === JSON.stringify(intended)) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async readStoredEvidence(input) {
|
|
368
|
+
try {
|
|
369
|
+
const parsed = JSON.parse((await this.dependencies.store.readEvidence(input.generationId, confirmationEvidencePath(input.challengeId))).toString("utf8"));
|
|
370
|
+
const evidence = GenerationConfirmationEvidenceSchema.parse(parsed);
|
|
371
|
+
if (evidence.proposal.binding.generationId !== input.generationId
|
|
372
|
+
|| evidence.snapshot.generationId !== input.generationId) {
|
|
373
|
+
throw new Error("Generation confirmation evidence belongs to another session");
|
|
374
|
+
}
|
|
375
|
+
return evidence;
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
throw bindingFailure(error instanceof Error
|
|
379
|
+
? `Generation confirmation evidence is unavailable: ${error.message}`
|
|
380
|
+
: "Generation confirmation evidence is unavailable");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type GenerationErrorCode, type GenerationMeta } from "../../domain/generation.js";
|
|
3
|
+
import { type Journey } from "../../domain/journey.js";
|
|
4
|
+
import { type ProjectContext } from "../../domain/project-context.js";
|
|
5
|
+
import { type TapHoundConfig } from "../../domain/config.js";
|
|
6
|
+
import type { AdbPort } from "../../ports/adb.js";
|
|
7
|
+
import { type GenerationSessionStore } from "../../ports/generation-session-store.js";
|
|
8
|
+
import type { ContextValidator } from "../context/context-validator.js";
|
|
9
|
+
import type { ProjectDescription } from "../project/project-describer.js";
|
|
10
|
+
import type { VerifyRuntime } from "../runtime/verify-runtime.js";
|
|
11
|
+
import { GenerationPublisher } from "./generation-publisher.js";
|
|
12
|
+
export declare const GenerationOutputPathSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
13
|
+
export type GenerationFinalizationStage = "precondition" | "verification" | "publication" | "export";
|
|
14
|
+
export declare class GenerationFinalizationError extends Error {
|
|
15
|
+
readonly code: GenerationErrorCode;
|
|
16
|
+
readonly stage: GenerationFinalizationStage;
|
|
17
|
+
readonly recoverable: boolean;
|
|
18
|
+
readonly name = "GenerationFinalizationError";
|
|
19
|
+
constructor(code: GenerationErrorCode, stage: GenerationFinalizationStage, recoverable: boolean, message: string, options?: ErrorOptions);
|
|
20
|
+
}
|
|
21
|
+
export interface GenerationFinalizeInput {
|
|
22
|
+
generationId: string;
|
|
23
|
+
projectRoot: string;
|
|
24
|
+
config: TapHoundConfig;
|
|
25
|
+
context: ProjectContext;
|
|
26
|
+
project: ProjectDescription;
|
|
27
|
+
outputPath: string;
|
|
28
|
+
name?: string | undefined;
|
|
29
|
+
deviceSerial: string;
|
|
30
|
+
toolVersions: Record<string, string>;
|
|
31
|
+
signal?: AbortSignal | undefined;
|
|
32
|
+
}
|
|
33
|
+
export interface GenerationFinalizerDependencies {
|
|
34
|
+
store: Pick<GenerationSessionStore, "read" | "beginVerification" | "completeVerification" | "failVerification" | "markBundlePublishable" | "writeTextEvidence" | "readEvidence">;
|
|
35
|
+
contextValidator: Pick<ContextValidator, "validate">;
|
|
36
|
+
adb: Pick<AdbPort, "forceStop">;
|
|
37
|
+
verifyRuntime: Pick<VerifyRuntime, "verify">;
|
|
38
|
+
publisher: GenerationPublisher;
|
|
39
|
+
generateAttemptId: () => string;
|
|
40
|
+
}
|
|
41
|
+
export interface GenerationFinalizeResult {
|
|
42
|
+
status: "verified";
|
|
43
|
+
journey: Journey;
|
|
44
|
+
meta: GenerationMeta;
|
|
45
|
+
bundlePath: string;
|
|
46
|
+
journeyPath: string;
|
|
47
|
+
metaPath: string;
|
|
48
|
+
replayed: boolean;
|
|
49
|
+
}
|
|
50
|
+
export declare class GenerationFinalizer {
|
|
51
|
+
private readonly dependencies;
|
|
52
|
+
constructor(dependencies: GenerationFinalizerDependencies);
|
|
53
|
+
readonly finalize: (input: GenerationFinalizeInput) => Promise<GenerationFinalizeResult>;
|
|
54
|
+
private assertBindings;
|
|
55
|
+
private revalidate;
|
|
56
|
+
private beginVerification;
|
|
57
|
+
private expectedVerification;
|
|
58
|
+
private assertEligible;
|
|
59
|
+
private assertReport;
|
|
60
|
+
private receiptMatches;
|
|
61
|
+
private persistPassedVerification;
|
|
62
|
+
private reconcilePassedVerification;
|
|
63
|
+
private persistFailedVerification;
|
|
64
|
+
private readVerifiedReport;
|
|
65
|
+
private buildMeta;
|
|
66
|
+
private ensureVerificationEvidence;
|
|
67
|
+
}
|