mtech-ai-local-runner 0.1.73 → 0.1.75
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/README.md +35 -0
- package/dist/src/adapters/claude-code/prompt-compiler.js +3 -1
- package/dist/src/adapters/codex/exec-invocation.js +24 -0
- package/dist/src/adapters/codex/exec-launch-host.js +5 -0
- package/dist/src/adapters/codex/exec-runtime-options.js +1 -1
- package/dist/src/adapters/codex/prompt-compiler.js +3 -1
- package/dist/src/asks/runtime.js +22 -1
- package/dist/src/daemon/execution-coordinator.js +65 -9
- package/dist/src/daemon/manual-codex.js +2 -2
- package/dist/src/daemon/runtime.js +31 -10
- package/dist/src/daemon/suspended-publication.js +52 -0
- package/dist/src/gateway-client.js +79 -5
- package/dist/src/protocol.js +33 -0
- package/dist/src/publication/installed-host.js +4 -2
- package/dist/src/publication/server-ci-host.js +85 -31
- package/dist/src/rooms/activity.js +84 -0
- package/dist/src/rooms/notifier.js +21 -0
- package/dist/src/rooms/store.js +56 -0
- package/dist/src/safety/claude-managed-scope.js +7 -19
- package/dist/src/safety/claude-release.js +0 -2
- package/dist/src/safety/claude-review-evidence.js +2 -2
- package/dist/src/safety/codex-exec-production-host.js +15 -2
- package/dist/src/safety/job-tool-policy.js +4 -3
- package/dist/src/verification/continuation-runner-contract.js +228 -0
- package/dist/src/verification/p1-execution-host.js +25 -6
- package/dist/src/verification/p1-host-composition.js +2 -0
- package/dist/src/verification/reviewed-runner-contract.js +7 -0
- package/dist/src/verification/runner-observed-verifier.js +29 -5
- package/dist/src/workspace/registered-submodules.js +204 -0
- package/dist/src/workspace/workspace-manager.js +128 -18
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertClaimedExecutionAttempt, assertBoundExecutionAttempt, CONTINUATION_SCHEMA, CONTINUATION_POLICY } from './verification/continuation-runner-contract.js';
|
|
1
2
|
import { publicationError } from './publication/failure.js';
|
|
2
3
|
import { issueTransportDiagnostic } from './safety/transport-diagnostic.js';
|
|
3
4
|
import { hasP1 } from './verification/p1-execution-host.js';
|
|
@@ -17,6 +18,18 @@ export class GatewayToolError extends Error {
|
|
|
17
18
|
this.code = code;
|
|
18
19
|
}
|
|
19
20
|
}
|
|
21
|
+
const ROOM_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
22
|
+
const SUSPENDED_JOB_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
23
|
+
/** A malformed answer is dropped, never a reason to refuse the sync: the runner then stays suspended. */
|
|
24
|
+
function parseSuspendedJob(value) {
|
|
25
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
26
|
+
return undefined;
|
|
27
|
+
const { job_id: jobId, generation, status, closed } = value;
|
|
28
|
+
if (typeof jobId !== "string" || !SUSPENDED_JOB_ID.test(jobId) || !Number.isSafeInteger(generation) || generation < 1
|
|
29
|
+
|| typeof status !== "string" || !/^[a-z_]{1,32}$/.test(status) || typeof closed !== "boolean")
|
|
30
|
+
return undefined;
|
|
31
|
+
return { jobId, generation: generation, status, closed };
|
|
32
|
+
}
|
|
20
33
|
const FIX_CONSENT_GRANT_ID = /^[a-zA-Z0-9-]{1,64}$/;
|
|
21
34
|
const FIX_CONSENT_REPOSITORY_KEY = /^[a-z0-9][a-z0-9._-]{0,99}$/;
|
|
22
35
|
const FIX_CONSENT_GRANTS_MAX = 20;
|
|
@@ -52,6 +65,7 @@ export function parseRunnerSync(result, runnerId) {
|
|
|
52
65
|
if (body === undefined || !Array.isArray(body.opens) || malformed("ask_deliveries") || malformed("closed_delivery_ids")) {
|
|
53
66
|
throw new GatewayToolError("Gateway returned an invalid runner sync result", "local_agent_runner_sync", "GATEWAY_INVALID_TOOL_RESULT");
|
|
54
67
|
}
|
|
68
|
+
const suspendedJob = parseSuspendedJob(body.suspended_job);
|
|
55
69
|
return {
|
|
56
70
|
opens: body.opens,
|
|
57
71
|
...(Array.isArray(body.ask_deliveries) ? { askDeliveries: body.ask_deliveries } : {}),
|
|
@@ -60,7 +74,9 @@ export function parseRunnerSync(result, runnerId) {
|
|
|
60
74
|
...(typeof body.guidance_policy_version === "string" ? { guidancePolicyVersion: body.guidance_policy_version } : {}),
|
|
61
75
|
...("claude_release" in body ? { claudeRelease: parseClaudeSyncRelease(body.claude_release) } : {}),
|
|
62
76
|
...(body.ask_alert === true ? { askAlert: true } : {}),
|
|
77
|
+
...(body.room_activity_enabled === true ? { roomActivityEnabled: true } : {}),
|
|
63
78
|
...(Array.isArray(body.fix_consent_grants) ? { fixConsentGrants: parseFixConsentGrants(body.fix_consent_grants) } : {}),
|
|
79
|
+
...(suspendedJob ? { suspendedJob } : {}),
|
|
64
80
|
jobAvailable: body.job_available === true,
|
|
65
81
|
push: parsePush(body.push, runnerId),
|
|
66
82
|
syncSeconds: clampSeconds(body.sync_seconds, 15, 120, 30),
|
|
@@ -116,7 +132,7 @@ export function shadowTaskPackage(contract) {
|
|
|
116
132
|
const verification = hasP1(payload) ? payload.verification : undefined;
|
|
117
133
|
if (hasP1(payload)) {
|
|
118
134
|
const resolved = resolveRunnerVerification(verification);
|
|
119
|
-
if (resolved.version
|
|
135
|
+
if (resolved.version !== 1 && (nested(payload, 'policy.policy_key') !== (resolved.version === 3 ? CONTINUATION_POLICY : REVIEWED_POLICY) || nested(payload, 'policy.loop_type') !== REVIEWED_LOOP_TYPE))
|
|
120
136
|
throw Error('P1_CONTRACT_REJECTED');
|
|
121
137
|
}
|
|
122
138
|
const repositories = (payload.repositories ?? []).map((repository) => ({
|
|
@@ -315,10 +331,14 @@ export class McpGatewayClient {
|
|
|
315
331
|
if (!this.checkoutPath) {
|
|
316
332
|
throw new Error("local checkout path is required before creating an execution binding");
|
|
317
333
|
}
|
|
318
|
-
const
|
|
334
|
+
const qualified = input.expectedJobId !== undefined || input.expectedAttemptNumber !== undefined;
|
|
335
|
+
if (qualified && (typeof input.expectedJobId !== 'string' || !input.expectedJobId.trim() || !Number.isSafeInteger(input.expectedAttemptNumber) || input.expectedAttemptNumber < 1 || input.expectedAttemptNumber > 3))
|
|
336
|
+
throw new GatewayToolError('Qualified next identity is invalid', 'work_loop_next', 'CORRECTION_LINEAGE_MISMATCH');
|
|
337
|
+
const nextArguments = { work_loop_id: input.workLoopId, ...(qualified ? { expected_job_id: input.expectedJobId, expected_attempt_number: input.expectedAttemptNumber } : {}) };
|
|
338
|
+
const useProvidedLease = Boolean(this.initialLeaseToken) && !input.claimNext && !qualified;
|
|
319
339
|
const claim = useProvidedLease
|
|
320
340
|
? await this.callTool("work_loop_show", { work_loop_id: input.workLoopId })
|
|
321
|
-
: await this.callTool("work_loop_next",
|
|
341
|
+
: await this.callTool("work_loop_next", nextArguments);
|
|
322
342
|
const leaseToken = useProvidedLease
|
|
323
343
|
? this.initialLeaseToken
|
|
324
344
|
: requiredString(claim.lease_token, "lease token");
|
|
@@ -505,12 +525,25 @@ export class McpGatewayClient {
|
|
|
505
525
|
return { leaseExpiresAt: context.leaseExpiresAt };
|
|
506
526
|
}
|
|
507
527
|
async waitForLocalJob(adapterVersions, waitSeconds = 25, jobId) {
|
|
508
|
-
|
|
528
|
+
const claim = await this.callTool("local_agent_jobs_wait", {
|
|
509
529
|
runner_id: this.runnerId,
|
|
510
530
|
adapter_versions: adapterVersions,
|
|
511
531
|
...(jobId ? { job_id: jobId } : {}),
|
|
512
532
|
wait_seconds: Math.max(0, Math.min(30, waitSeconds)),
|
|
513
533
|
});
|
|
534
|
+
if (['continuation', 'execution_attempt', 'execution_attempt_fingerprint'].some(key => Object.hasOwn(claim, key)))
|
|
535
|
+
throw new GatewayToolError('Unexpected claimed attempt identity', 'local_agent_jobs_wait', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
536
|
+
if (claim.status === 'claimed' && claim.task_package && claim.job) {
|
|
537
|
+
try {
|
|
538
|
+
assertClaimedExecutionAttempt({ id: claim.job.id, generation: claim.job.generation, workLoopId: claim.job.work_loop_id, launchFingerprint: claim.job.launch_fingerprint, launchGrant: claim.launch_grant ?? '', leaseToken: claim.lease_token ?? '', taskPackage: claim.task_package, executionAttempt: claim.attempt_context, executionAttemptFingerprint: claim.attempt_context_fingerprint });
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
throw new GatewayToolError('Claimed attempt identity is invalid', 'local_agent_jobs_wait', 'CORRECTION_EXECUTION_UNAVAILABLE');
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else if (claim.attempt_context !== undefined || claim.attempt_context_fingerprint !== undefined)
|
|
545
|
+
throw new GatewayToolError('Unexpected claimed attempt identity', 'local_agent_jobs_wait', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
546
|
+
return claim;
|
|
514
547
|
}
|
|
515
548
|
async registerLocalRunner(input) {
|
|
516
549
|
return this.callTool("local_agent_runner_register", {
|
|
@@ -587,9 +620,41 @@ export class McpGatewayClient {
|
|
|
587
620
|
reason: input.reason,
|
|
588
621
|
...(acknowledged.length > 0 ? { acknowledged_closure_ids: acknowledged } : {}),
|
|
589
622
|
...(input.guidancePolicyVersion ? { guidance_policy_version: input.guidancePolicyVersion } : {}),
|
|
623
|
+
...(input.roomActivityV1 ? { room_activity_v1: true } : {}),
|
|
624
|
+
...(input.suspendedJob ? { suspended_job: { job_id: input.suspendedJob.jobId, generation: input.suspendedJob.generation } } : {}),
|
|
590
625
|
}, signal);
|
|
591
626
|
return parseRunnerSync(result, this.runnerId);
|
|
592
627
|
}
|
|
628
|
+
async readRoomActivity(afterRoomId = '', limit = 50, signal) {
|
|
629
|
+
if ((afterRoomId !== '' && !ROOM_ID.test(afterRoomId)) || !Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
630
|
+
throw new GatewayToolError('Invalid Room activity query', 'work_room_read', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
631
|
+
}
|
|
632
|
+
const result = await this.callTool('work_room_read', { operation: 'activity', after_room_id: afterRoomId, limit, share_only: true }, signal);
|
|
633
|
+
const body = result && typeof result === 'object' && !Array.isArray(result) ? result : undefined;
|
|
634
|
+
if (!body || !Array.isArray(body.rooms) || body.rooms.length > limit
|
|
635
|
+
|| !(body.next_cursor === null || (typeof body.next_cursor === 'string' && ROOM_ID.test(body.next_cursor)))) {
|
|
636
|
+
throw new GatewayToolError('Invalid Room activity page', 'work_room_read', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
637
|
+
}
|
|
638
|
+
const rooms = body.rooms.map(row => {
|
|
639
|
+
const item = row && typeof row === 'object' && !Array.isArray(row) ? row : {};
|
|
640
|
+
if (typeof item.room_id !== 'string' || !ROOM_ID.test(item.room_id)
|
|
641
|
+
|| !Number.isSafeInteger(item.unread_count) || item.unread_count < 0
|
|
642
|
+
|| !Number.isSafeInteger(item.latest_message_sequence) || item.latest_message_sequence < 0) {
|
|
643
|
+
throw new GatewayToolError('Invalid Room activity entry', 'work_room_read', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
644
|
+
}
|
|
645
|
+
return { roomId: item.room_id, unreadCount: item.unread_count, latestMessageSequence: item.latest_message_sequence };
|
|
646
|
+
});
|
|
647
|
+
let previous = afterRoomId;
|
|
648
|
+
for (const room of rooms) {
|
|
649
|
+
if (room.roomId <= previous)
|
|
650
|
+
throw new GatewayToolError('Room activity order is invalid', 'work_room_read', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
651
|
+
previous = room.roomId;
|
|
652
|
+
}
|
|
653
|
+
if (body.next_cursor !== null && (rooms.length === 0 || body.next_cursor !== previous)) {
|
|
654
|
+
throw new GatewayToolError('Room activity cursor is invalid', 'work_room_read', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
655
|
+
}
|
|
656
|
+
return { rooms, nextCursor: body.next_cursor };
|
|
657
|
+
}
|
|
593
658
|
async authorizeRunnerChannel(socketId, signal) {
|
|
594
659
|
const result = await this.callTool("local_agent_runner_channel_auth", {
|
|
595
660
|
runner_id: this.runnerId,
|
|
@@ -640,6 +705,15 @@ export class McpGatewayClient {
|
|
|
640
705
|
const bindingId = requiredString(result.binding.id, "binding ID");
|
|
641
706
|
const iterationId = requiredString(result.binding.iteration_id, "iteration ID");
|
|
642
707
|
signal?.throwIfAborted();
|
|
708
|
+
if (input.claimedJob) {
|
|
709
|
+
if (input.claimedJob.id !== input.jobId || input.claimedJob.generation !== input.generation || input.claimedJob.workLoopId !== input.workLoopId || input.claimedJob.leaseToken !== input.jobLeaseToken || input.claimedJob.launchGrant !== input.launchGrant || result.work_loop_id !== input.workLoopId)
|
|
710
|
+
throw Error('CORRECTION_LINEAGE_MISMATCH');
|
|
711
|
+
assertBoundExecutionAttempt(input.claimedJob, result.bundle, iterationId);
|
|
712
|
+
if (input.claimedJob.executionAttempt && typeof result.binding_replayed !== 'boolean')
|
|
713
|
+
throw Error('CORRECTION_BINDING_RECONCILIATION_REQUIRED');
|
|
714
|
+
}
|
|
715
|
+
else if (Object.hasOwn(result.bundle, 'attempt_context') || Object.hasOwn(result.bundle, 'attempt_context_fingerprint') || nested(result.bundle, 'contract.verification.schema_version') === CONTINUATION_SCHEMA)
|
|
716
|
+
throw Error('CORRECTION_LINEAGE_MISMATCH');
|
|
643
717
|
this.context = {
|
|
644
718
|
jobFence: { job_id: input.jobId, generation: input.generation, lease_token: input.jobLeaseToken },
|
|
645
719
|
workLoopId: requiredString(result.work_loop_id, "Work Loop ID"),
|
|
@@ -653,7 +727,7 @@ export class McpGatewayClient {
|
|
|
653
727
|
runtimeAdapterVersion: requiredString(result.binding.runtime_adapter_version, "runtime adapter version"),
|
|
654
728
|
};
|
|
655
729
|
return {
|
|
656
|
-
bindingId,
|
|
730
|
+
bindingId, iterationId, ...(input.claimedJob?.executionAttempt ? { bindingReplayed: result.binding_replayed } : {}),
|
|
657
731
|
taskPackage: result.bundle,
|
|
658
732
|
resolvedModelId: requiredString(result.binding.resolved_model_id, "resolved model ID"),
|
|
659
733
|
};
|
package/dist/src/protocol.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { validateAttemptContext, attemptContextFingerprint } from './verification/continuation-runner-contract.js';
|
|
2
|
+
import { protocolHash } from './publication/candidate.js';
|
|
1
3
|
export const PROTOCOL_VERSION = "agent-execution-v1";
|
|
2
4
|
export const ROLES = ["planner", "supervisor", "executor", "reviewer"];
|
|
3
5
|
export const EVENT_TYPES = [
|
|
@@ -73,3 +75,34 @@ export class TerminalGuard {
|
|
|
73
75
|
return this.terminalSeen !== null;
|
|
74
76
|
}
|
|
75
77
|
}
|
|
78
|
+
/** Wire-domain/identity check only. Scope, measured command results and grant authority
|
|
79
|
+
* are verified by their owners; a syntactically valid payload is not completion proof. */
|
|
80
|
+
export function validateAttemptEvidence(value, expected) {
|
|
81
|
+
const c = validateAttemptContext(expected), fp = attemptContextFingerprint(c);
|
|
82
|
+
const reject = () => { throw Error('CORRECTION_LINEAGE_MISMATCH'); };
|
|
83
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
84
|
+
reject();
|
|
85
|
+
const p = value, m = p.changed_paths, receipts = p.receipts;
|
|
86
|
+
const hex = (v, n) => typeof v === 'string' && new RegExp(`^[a-f0-9]{${n}}$`).test(v);
|
|
87
|
+
if (!hex(p.candidate_commit_sha, 40) || !hex(p.snapshot_fingerprint, 64) || !m || m.schema_version !== 'runner-changed-paths-v2' || m.digest_domain !== 'p1-changed-paths-v2' || m.attempt_context_fingerprint !== fp || m.base_commit_sha !== c.attempt_base_commit_sha || m.candidate_commit_sha !== p.candidate_commit_sha || !Array.isArray(m.entries) || !m.entries.length || !Array.isArray(receipts) || !receipts.length)
|
|
88
|
+
reject();
|
|
89
|
+
const { manifest_sha256, ...unsigned } = m;
|
|
90
|
+
if (manifest_sha256 !== protocolHash(unsigned))
|
|
91
|
+
reject();
|
|
92
|
+
for (const value of receipts) {
|
|
93
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
94
|
+
reject();
|
|
95
|
+
const r = value;
|
|
96
|
+
if (r.schema_version !== 'runner-command-receipt-v2' || r.attempt_context_fingerprint !== fp || r.job_id !== c.job_id || r.generation !== c.generation || r.contract_fingerprint !== c.contract_fingerprint || r.candidate_commit_sha !== p.candidate_commit_sha || r.snapshot_fingerprint !== p.snapshot_fingerprint)
|
|
97
|
+
reject();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Versioned host handoff check, distinct from ordinary terminal event transport. */
|
|
101
|
+
export function validateAttemptHandoff(value, expected) {
|
|
102
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
103
|
+
throw Error('CORRECTION_LINEAGE_MISMATCH');
|
|
104
|
+
const h = value, fp = attemptContextFingerprint(expected);
|
|
105
|
+
if (h.schema_version !== 'gateway-candidate-handoff-v2' || h.attempt_context_fingerprint !== fp || attemptContextFingerprint(h.attempt_context) !== fp)
|
|
106
|
+
throw Error('CORRECTION_LINEAGE_MISMATCH');
|
|
107
|
+
validateAttemptEvidence(h, expected);
|
|
108
|
+
}
|
|
@@ -42,6 +42,8 @@ export function assertLocalFixConsent(value) {
|
|
|
42
42
|
if (!Array.isArray(modes) || !modes.length || new Set(modes).size !== modes.length || modes.some(m => m !== 'gateway_ci_v1' && m !== 'gateway_publish_v1'))
|
|
43
43
|
throw denied();
|
|
44
44
|
}
|
|
45
|
+
/** One fixed JSON line per repeated publication call; no payload, path or token. */
|
|
46
|
+
const reportRetry = (event) => { process.stderr.write(`${JSON.stringify(event)}\n`); };
|
|
45
47
|
/** Serves each job with the registration of the mode frozen in its own Fix scope. */
|
|
46
48
|
function multiModeHost(hosts) {
|
|
47
49
|
const verificationModes = hosts.map(h => { if (h.verificationMode === undefined)
|
|
@@ -94,7 +96,7 @@ export async function loadInstalledLocalFixHost(input) {
|
|
|
94
96
|
// The scope's own reviewed policy and this measured release make the registration the server CI host checks the job against.
|
|
95
97
|
const manifest = { verification_mode: mode, project: key, ci_profile: scope.ci_profile, ci_profile_fingerprint: protocolHash(scope.ci_profile), receipt: { ...measured.receipt } };
|
|
96
98
|
const registration = { ...manifest, review: { status: 'reviewed', fingerprint: protocolHash(manifest) } };
|
|
97
|
-
return createServerCiLocalFixHost({ runnerId: input.runnerId, privateRoot, git: config.git, registration, gateway: input.gateway, assertReleaseCurrent, canonicalApprovalId: async (publicationId) => {
|
|
99
|
+
return createServerCiLocalFixHost({ runnerId: input.runnerId, privateRoot, git: config.git, registration, gateway: input.gateway, assertReleaseCurrent, report: reportRetry, canonicalApprovalId: async (publicationId) => {
|
|
98
100
|
let approvals;
|
|
99
101
|
try {
|
|
100
102
|
approvals = await readPrivateJson(path.join(input.stateRoot, 'local-fix-pr-approvals.json'));
|
|
@@ -146,7 +148,7 @@ export async function loadInstalledLocalFixHost(input) {
|
|
|
146
148
|
const privateRoot = path.join(input.stateRoot, 'local-fix-publication'), s = await lstat(privateRoot);
|
|
147
149
|
if (!s.isDirectory() || s.isSymbolicLink() || (s.mode & 0o077) !== 0 || (process.getuid && s.uid !== process.getuid()) || await realpath(privateRoot) !== privateRoot)
|
|
148
150
|
throw denied();
|
|
149
|
-
const hosts = registrations.map(registration => createServerCiLocalFixHost({ runnerId: input.runnerId, privateRoot, git: config.git, registration, gateway: input.gateway, assertReleaseCurrent, canonicalApprovalId: async (publicationId) => {
|
|
151
|
+
const hosts = registrations.map(registration => createServerCiLocalFixHost({ runnerId: input.runnerId, privateRoot, git: config.git, registration, gateway: input.gateway, assertReleaseCurrent, report: reportRetry, canonicalApprovalId: async (publicationId) => {
|
|
150
152
|
let approvals;
|
|
151
153
|
try {
|
|
152
154
|
approvals = await readPrivateJson(path.join(input.stateRoot, 'local-fix-pr-approvals.json'));
|
|
@@ -14,6 +14,13 @@ import { GitSnapshotHost, SNAPSHOT_TREE_MAX_BYTES } from './git-snapshot.js';
|
|
|
14
14
|
import { PublicationOutcomeUnknownError } from './publisher.js';
|
|
15
15
|
import { isLocalFixAdapter } from './local-fix-adapters.js';
|
|
16
16
|
const denied = () => Error('LOCAL_FIX_SERVER_CI_REJECTED');
|
|
17
|
+
/** Work Decision b3d7eb22: a failed Gateway publication call is repeated for at most this long; the coordinator heartbeat keeps the lease. */
|
|
18
|
+
export const PUBLICATION_RETRY_WINDOW_MS = 900_000;
|
|
19
|
+
/** 5 s doubling to 130 s, longer than the Gateway's 120-s quiet window, so an absent working branch can be staged again (decision 1bddc563). */
|
|
20
|
+
export const publicationRetryDelay = (failures) => Math.min(5000 * 2 ** (failures - 1), 130_000);
|
|
21
|
+
/** Only the Gateway read of the fence failed; every local check passed, so the call may be repeated. */
|
|
22
|
+
class FenceUnreadable extends Error {
|
|
23
|
+
}
|
|
17
24
|
const safePath = (p) => typeof p === 'string' && p.length > 0 && !p.includes('\\') && !p.includes('\0') && !path.isAbsolute(p) && p.split('/').every(s => s !== '' && s !== '.' && s !== '..' && !/^(?:\.git|\.mcp(?:\..*)?|\.npmrc|\.netrc|id_rsa|id_ed25519|\.local-runner|\.runner|\.env(?:\..*)?|\.codex|\.claude|\.ssh|\.aws|\.config|auth\.(?:json|toml|ya?ml)(?:\..*)?|credentials(?:\..*)?|AGENTS\.md|CLAUDE\.md|mcp\.(?:json|toml|ya?ml)(?:\..*)?)$/i.test(s));
|
|
18
25
|
/** Gateway decision 8e7fff53: under any directory scope, test infrastructure, boot and dependency files stay as the base has them. */
|
|
19
26
|
const testInfrastructure = (p) => /(^|\/)(phpunit\.xml(\.dist)?|bitbucket-pipelines\.yml|composer\.[^/]+|package(-lock)?\.json|yarn\.lock|pnpm-lock\.yaml)$|^(scripts|app\/common)(\/|$)|(^|\/)(vendor|node_modules)(\/|$)/.test(p);
|
|
@@ -82,8 +89,22 @@ export function createServerCiLocalFixHost(options) {
|
|
|
82
89
|
throw denied();
|
|
83
90
|
let closed = false, settled = false, stageStarted = false, host, phase = 'validation_report', primary, disposing, finalization, seal;
|
|
84
91
|
const current = async () => { if (closed || !lifecycle.isCurrent() || !lifecycle.isFrozen())
|
|
85
|
-
throw denied(); await options.assertReleaseCurrent?.();
|
|
92
|
+
throw denied(); await options.assertReleaseCurrent?.(); let context; try {
|
|
93
|
+
context = (await options.gateway.localPublicationContext(fence)).context;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new FenceUnreadable();
|
|
97
|
+
} if (protocolHash(bindingProof(context)) !== protocolHash(binding) || closed || !lifecycle.isCurrent() || !lifecycle.isFrozen())
|
|
86
98
|
throw denied(); await seal?.assertUnchanged(); };
|
|
99
|
+
const readable = async () => { try {
|
|
100
|
+
await current();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error instanceof FenceUnreadable)
|
|
105
|
+
return false;
|
|
106
|
+
throw error;
|
|
107
|
+
} };
|
|
87
108
|
const dispose = () => disposing ??= (host?.dispose() ?? Promise.resolve());
|
|
88
109
|
return { close: async () => { closed = true; if (!finalization || settled) {
|
|
89
110
|
try {
|
|
@@ -101,8 +122,11 @@ export function createServerCiLocalFixHost(options) {
|
|
|
101
122
|
phase = 'current_fence';
|
|
102
123
|
await current();
|
|
103
124
|
host = new GitSnapshotHost({ root: input.checkoutPath, privateRoot: root, git: options.git, baseCommitSha: binding.base_commit_sha, workingBranch: binding.working_branch, maxFileBytes: SNAPSHOT_TREE_MAX_BYTES, maxTotalBytes: SNAPSHOT_TREE_MAX_BYTES });
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
// materialize captures once, checks live source again after copying, and seals every private byte.
|
|
126
|
+
// Use that same observation for limits and upload; a separate observe() repeats two full captures.
|
|
127
|
+
phase = 'snapshot_materialize';
|
|
128
|
+
seal = await host.materialize();
|
|
129
|
+
const observed = structuredClone(seal.observation);
|
|
106
130
|
phase = 'candidate_limits';
|
|
107
131
|
let split;
|
|
108
132
|
if (limits.version === 2) {
|
|
@@ -118,10 +142,6 @@ export function createServerCiLocalFixHost(options) {
|
|
|
118
142
|
throw publicationError(phase, 'LOCAL_FIX_DIFF_LIMIT_REJECTED');
|
|
119
143
|
if (!observed.changed_files.length || observed.changed_files.length > limits.max_changed_files)
|
|
120
144
|
throw publicationError(phase, 'LOCAL_FIX_FILE_COUNT_REJECTED');
|
|
121
|
-
phase = 'snapshot_materialize';
|
|
122
|
-
seal = await host.materialize();
|
|
123
|
-
if (protocolHash(observed) !== protocolHash(seal.observation))
|
|
124
|
-
throw denied();
|
|
125
145
|
// The v1 wire represents ordinary text bytes only. Refuse any change whose
|
|
126
146
|
// executable/type metadata would be lost by the provider's regular-file write.
|
|
127
147
|
phase = 'delta_format';
|
|
@@ -199,21 +219,51 @@ export function createServerCiLocalFixHost(options) {
|
|
|
199
219
|
if (!security)
|
|
200
220
|
throw denied();
|
|
201
221
|
let result;
|
|
222
|
+
const sleep = options.sleep ?? delay, now = options.now ?? Date.now;
|
|
223
|
+
// Work Decision b3d7eb22: a string answer names a failed Gateway call, repeated with the same arguments while the
|
|
224
|
+
// fence holds. The streak ends at the first accepted answer; a delay that would outlast the window gives up as before.
|
|
225
|
+
const retrying = async (attempt) => {
|
|
226
|
+
let started;
|
|
227
|
+
for (let failures = 1;; failures++) {
|
|
228
|
+
const answer = await attempt();
|
|
229
|
+
if (typeof answer !== 'string')
|
|
230
|
+
return answer;
|
|
231
|
+
started ??= now();
|
|
232
|
+
const wait = publicationRetryDelay(failures);
|
|
233
|
+
if (now() - started + wait > PUBLICATION_RETRY_WINDOW_MS)
|
|
234
|
+
throw new PublicationOutcomeUnknownError();
|
|
235
|
+
try {
|
|
236
|
+
options.report?.({ event: 'local_fix_publication_retry', operation: answer, failures, delay_ms: wait });
|
|
237
|
+
}
|
|
238
|
+
catch { /* diagnostics only */ }
|
|
239
|
+
await sleep(wait);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
202
242
|
phase = 'stage';
|
|
203
243
|
stageStarted = true;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
244
|
+
let stages = 0;
|
|
245
|
+
const stageInput = { ...fence, operation: 'stage', files, ...(report ? { validationReport: report } : {}), securityReceipt: { ...security }, hostReceipt: { ...registration.receipt } };
|
|
246
|
+
// The same frozen files again: the Gateway reconciles a present branch or stages an absent one again (1bddc563).
|
|
247
|
+
result = await retrying(async () => {
|
|
248
|
+
if (stages++ > 0 && !await readable())
|
|
249
|
+
return 'fence';
|
|
250
|
+
let answer;
|
|
251
|
+
try {
|
|
252
|
+
answer = (await options.gateway.executeServerCiPublication(stageInput)).result;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return 'stage';
|
|
256
|
+
}
|
|
257
|
+
const id = answer.publication_id;
|
|
258
|
+
if (typeof id !== 'string' || !id)
|
|
259
|
+
throw new PublicationOutcomeUnknownError();
|
|
260
|
+
if (record.publication_id && record.publication_id !== id)
|
|
261
|
+
throw new PublicationOutcomeUnknownError();
|
|
262
|
+
record.publication_id = id;
|
|
263
|
+
await journal.save(key, record);
|
|
264
|
+
return answer.status === 'publication_unknown' ? 'stage' : answer;
|
|
265
|
+
});
|
|
266
|
+
const publicationId = String(result.publication_id);
|
|
217
267
|
const pending = new Set(['staged', 'validating', 'authorized', 'committing', 'committed', 'pushing', 'pushed', 'opening_pr']);
|
|
218
268
|
for (;;) {
|
|
219
269
|
phase = 'result';
|
|
@@ -222,7 +272,7 @@ export function createServerCiLocalFixHost(options) {
|
|
|
222
272
|
if (result.status === 'ci_failed')
|
|
223
273
|
throw new ServerCiPublicationError(result.ci?.error_code ?? result.failure?.code);
|
|
224
274
|
if (result.status === 'completed') {
|
|
225
|
-
await
|
|
275
|
+
await retrying(async () => await readable() ? {} : 'fence');
|
|
226
276
|
const ci = result.ci;
|
|
227
277
|
if (result.verification_mode !== mode || (mode === 'gateway_ci_v1' ? ci?.status !== 'passed' || ci.commit_sha !== result.commit_sha : ci?.status !== 'not_run' || result.verification?.status !== 'unverified') || typeof result.commit_sha !== 'string' || !/^[a-f0-9]{40}$/.test(result.commit_sha) || result.working_branch !== binding.working_branch)
|
|
228
278
|
throw new PublicationOutcomeUnknownError();
|
|
@@ -231,16 +281,20 @@ export function createServerCiLocalFixHost(options) {
|
|
|
231
281
|
if (result.waiting || !pending.has(String(result.status)))
|
|
232
282
|
throw new PublicationOutcomeUnknownError();
|
|
233
283
|
phase = 'advance';
|
|
234
|
-
await
|
|
235
|
-
await
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
284
|
+
await sleep(5000);
|
|
285
|
+
result = await retrying(async () => {
|
|
286
|
+
if (!await readable())
|
|
287
|
+
return 'fence';
|
|
288
|
+
const approval = await options.canonicalApprovalId?.(publicationId);
|
|
289
|
+
if (!await readable())
|
|
290
|
+
return 'fence';
|
|
291
|
+
try {
|
|
292
|
+
return (await options.gateway.executeServerCiPublication({ ...fence, operation: 'advance', publicationId, canonicalApprovalId: approval })).result;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return 'advance';
|
|
296
|
+
}
|
|
297
|
+
});
|
|
244
298
|
}
|
|
245
299
|
});
|
|
246
300
|
})().catch(error => {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { RoomActivityStore } from './store.js';
|
|
2
|
+
export const ROOM_CHAT_LINE = 'AI Gateway notice: Tell the person new Work Room activity is available. Read the Room only if they ask; do not reply automatically.';
|
|
3
|
+
/** One actor-owned projection. Scans serialize so a signal and periodic sync cannot race the watermark file. */
|
|
4
|
+
export class RoomActivityRuntime {
|
|
5
|
+
deps;
|
|
6
|
+
store;
|
|
7
|
+
queue = Promise.resolve();
|
|
8
|
+
constructor(deps) {
|
|
9
|
+
this.deps = deps;
|
|
10
|
+
this.store = new RoomActivityStore(deps.stateRoot);
|
|
11
|
+
}
|
|
12
|
+
exclusive(work) {
|
|
13
|
+
const result = this.queue.then(work);
|
|
14
|
+
this.queue = result.then(() => undefined, () => undefined);
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
async handleSync(enabled) {
|
|
18
|
+
await this.exclusive(async () => {
|
|
19
|
+
const state = await this.store.load();
|
|
20
|
+
if (!enabled) {
|
|
21
|
+
if (state.pendingChat.length || state.pendingOs.length) {
|
|
22
|
+
state.pendingChat = [];
|
|
23
|
+
state.pendingOs = [];
|
|
24
|
+
await this.store.save(state);
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const rows = [];
|
|
29
|
+
const cursors = new Set();
|
|
30
|
+
let cursor = state.scanCursor ?? '';
|
|
31
|
+
let complete = false;
|
|
32
|
+
for (let page = 0; page < 20; page++) {
|
|
33
|
+
if (cursors.has(cursor))
|
|
34
|
+
throw new Error('ROOM_ACTIVITY_CURSOR_LOOP');
|
|
35
|
+
cursors.add(cursor);
|
|
36
|
+
const result = await this.deps.read(cursor, 50);
|
|
37
|
+
rows.push(...result.rooms);
|
|
38
|
+
if (result.nextCursor === null) {
|
|
39
|
+
cursor = '';
|
|
40
|
+
complete = true;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
cursor = result.nextCursor;
|
|
44
|
+
}
|
|
45
|
+
const visible = new Set([...(state.scanVisible ?? []), ...rows.map(row => row.roomId)]);
|
|
46
|
+
const next = {
|
|
47
|
+
version: 1, seen: complete ? {} : { ...state.seen },
|
|
48
|
+
pendingOs: complete ? state.pendingOs.filter(id => visible.has(id)) : [...state.pendingOs],
|
|
49
|
+
pendingChat: complete ? state.pendingChat.filter(id => visible.has(id)) : [...state.pendingChat],
|
|
50
|
+
...(complete ? {} : { scanCursor: cursor, scanVisible: [...visible] }),
|
|
51
|
+
};
|
|
52
|
+
if (complete) {
|
|
53
|
+
for (const id of visible)
|
|
54
|
+
if (id in state.seen)
|
|
55
|
+
next.seen[id] = state.seen[id];
|
|
56
|
+
}
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
const previous = state.seen[row.roomId] ?? 0;
|
|
59
|
+
next.seen[row.roomId] = Math.max(previous, row.latestMessageSequence);
|
|
60
|
+
if (row.unreadCount > 0 && row.latestMessageSequence > previous) {
|
|
61
|
+
if (!next.pendingOs.includes(row.roomId))
|
|
62
|
+
next.pendingOs.push(row.roomId);
|
|
63
|
+
if (!next.pendingChat.includes(row.roomId))
|
|
64
|
+
next.pendingChat.push(row.roomId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
await this.store.save(next);
|
|
68
|
+
if (next.pendingOs.length && await this.deps.notify()) {
|
|
69
|
+
next.pendingOs = [];
|
|
70
|
+
await this.store.save(next);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async claimNotice() {
|
|
75
|
+
return this.exclusive(async () => {
|
|
76
|
+
const state = await this.store.load();
|
|
77
|
+
if (!state.pendingChat.length)
|
|
78
|
+
return null;
|
|
79
|
+
state.pendingChat = [];
|
|
80
|
+
await this.store.save(state);
|
|
81
|
+
return ROOM_CHAT_LINE;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
export const ROOM_OS_LINE = 'AI Gateway: new Work Room activity. Open your AI chat to review it.';
|
|
3
|
+
const SCRIPT = ['on run argv', 'display notification (item 1 of argv) with title "AI Gateway"', 'end run'];
|
|
4
|
+
export function createRoomNotifier(deps = {}) {
|
|
5
|
+
const run = deps.run ?? ((file, args) => new Promise((resolve, reject) => {
|
|
6
|
+
execFile(file, args, { timeout: 3000, killSignal: 'SIGKILL' }, error => error ? reject(error) : resolve());
|
|
7
|
+
}));
|
|
8
|
+
return async () => {
|
|
9
|
+
if ((deps.platform ?? process.platform) !== 'darwin')
|
|
10
|
+
return false;
|
|
11
|
+
try {
|
|
12
|
+
await run('/usr/bin/osascript', [...SCRIPT.flatMap(line => ['-e', line]), ROOM_OS_LINE]);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Generic macOS notice. No Room metadata enters the command line or error log. */
|
|
21
|
+
export const notifyRoomActivity = createRoomNotifier();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const EMPTY = () => ({ version: 1, seen: {}, pendingOs: [], pendingChat: [] });
|
|
5
|
+
const ROOM_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
|
+
export class RoomActivityStore {
|
|
7
|
+
stateRoot;
|
|
8
|
+
path;
|
|
9
|
+
constructor(stateRoot) {
|
|
10
|
+
this.stateRoot = stateRoot;
|
|
11
|
+
this.path = join(stateRoot, 'room-activity.json');
|
|
12
|
+
}
|
|
13
|
+
async load() {
|
|
14
|
+
let content;
|
|
15
|
+
try {
|
|
16
|
+
content = await fs.readFile(this.path, 'utf8');
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code === 'ENOENT')
|
|
20
|
+
return EMPTY();
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
const raw = JSON.parse(content);
|
|
24
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
25
|
+
throw new Error('ROOM_ACTIVITY_STATE_INVALID');
|
|
26
|
+
const state = raw;
|
|
27
|
+
if (state.version !== 1 || !state.seen || typeof state.seen !== 'object' || Array.isArray(state.seen)
|
|
28
|
+
|| !Array.isArray(state.pendingOs) || !Array.isArray(state.pendingChat))
|
|
29
|
+
throw new Error('ROOM_ACTIVITY_STATE_INVALID');
|
|
30
|
+
for (const [id, sequence] of Object.entries(state.seen)) {
|
|
31
|
+
if (!ROOM_ID.test(id) || !Number.isSafeInteger(sequence) || sequence < 0)
|
|
32
|
+
throw new Error('ROOM_ACTIVITY_STATE_INVALID');
|
|
33
|
+
}
|
|
34
|
+
for (const id of [...state.pendingOs, ...state.pendingChat]) {
|
|
35
|
+
if (typeof id !== 'string' || !ROOM_ID.test(id) || !(id in state.seen))
|
|
36
|
+
throw new Error('ROOM_ACTIVITY_STATE_INVALID');
|
|
37
|
+
}
|
|
38
|
+
if (state.scanCursor !== undefined && (typeof state.scanCursor !== 'string' || !ROOM_ID.test(state.scanCursor)))
|
|
39
|
+
throw new Error('ROOM_ACTIVITY_STATE_INVALID');
|
|
40
|
+
if (state.scanVisible !== undefined && (!Array.isArray(state.scanVisible)
|
|
41
|
+
|| state.scanVisible.some(id => typeof id !== 'string' || !ROOM_ID.test(id))))
|
|
42
|
+
throw new Error('ROOM_ACTIVITY_STATE_INVALID');
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
async save(state) {
|
|
46
|
+
await fs.mkdir(this.stateRoot, { recursive: true, mode: 0o700 });
|
|
47
|
+
const temp = `${this.path}.${randomUUID()}.tmp`;
|
|
48
|
+
try {
|
|
49
|
+
await fs.writeFile(temp, JSON.stringify(state), { mode: 0o600, flag: 'wx' });
|
|
50
|
+
await fs.rename(temp, this.path);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await fs.rm(temp, { force: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { lstat, readdir } from 'node:fs/promises';
|
|
4
|
-
import {
|
|
5
|
-
import { CLAUDE_MANAGED_PREFERENCE_DOMAIN, CLAUDE_MANAGED_SETTINGS_DIRECTORY, CLAUDE_MANAGED_SETTINGS_FILES, CLAUDE_POLICY_OBSERVER, claudeCanonicalSha256 } from './claude-release.js';
|
|
4
|
+
import { CLAUDE_MANAGED_PREFERENCE_DOMAIN, CLAUDE_MANAGED_SETTINGS_DIRECTORY, CLAUDE_MANAGED_SETTINGS_FILES, claudeCanonicalSha256 } from './claude-release.js';
|
|
6
5
|
const unverified = () => Error('CLAUDE_MANAGED_SCOPE_UNVERIFIED');
|
|
7
6
|
/** Claude managed sources the Runner checks on macOS (gate managed_scope_exclusion, scope unmanaged_personal_only). */
|
|
8
7
|
export function claudeManagedPaths(user, root = '/') {
|
|
@@ -50,38 +49,27 @@ export async function assertClaudeManagedPathsAbsent(user, root = '/') {
|
|
|
50
49
|
if (entries.length !== 0)
|
|
51
50
|
throw unverified();
|
|
52
51
|
}
|
|
53
|
-
/** Compiled defaults: the
|
|
52
|
+
/** Compiled defaults: the real managed locations. */
|
|
54
53
|
export function claudeManagedScopeDeps(root = '/') {
|
|
55
|
-
return {
|
|
56
|
-
checkReady: async () => { verifyPolicyObserver(CLAUDE_POLICY_OBSERVER); await createPolicyInventoryClient(CLAUDE_POLICY_OBSERVER).checkReady(); },
|
|
57
|
-
collect: () => createPolicyInventoryClient(CLAUDE_POLICY_OBSERVER).collectOnce(),
|
|
58
|
-
managedPathsAbsent: () => assertClaudeManagedPathsAbsent(os.userInfo().username, root),
|
|
59
|
-
};
|
|
54
|
+
return { managedPathsAbsent: () => assertClaudeManagedPathsAbsent(os.userInfo().username, root) };
|
|
60
55
|
}
|
|
61
56
|
export async function claudeManagedScopeReady(deps) {
|
|
62
57
|
try {
|
|
63
|
-
await deps.checkReady();
|
|
64
58
|
await deps.managedPathsAbsent();
|
|
65
59
|
}
|
|
66
60
|
catch {
|
|
67
61
|
throw unverified();
|
|
68
62
|
}
|
|
69
63
|
}
|
|
70
|
-
/** Per-job managed scope:
|
|
71
|
-
* plus absent Claude managed files. Managed, partial, unknown or stale input blocks. */
|
|
64
|
+
/** Per-job managed scope: absent Claude managed files and managed preferences. Present or unreadable input blocks. */
|
|
72
65
|
export async function collectClaudeManagedScope(deps) {
|
|
73
66
|
try {
|
|
74
|
-
const value = await deps.collect(), now = (deps.now ?? Date.now)();
|
|
75
|
-
const r = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
76
|
-
const observedAt = r.observedAt;
|
|
77
|
-
if (r.complete !== true || r.empty !== true || r.scope !== 'machine_and_current_user' || typeof observedAt !== 'number' || !Number.isSafeInteger(observedAt)
|
|
78
|
-
|| observedAt > now || now - observedAt > 60_000 || typeof r.inventoryFingerprint !== 'string' || !/^[a-f0-9]{64}$/.test(r.inventoryFingerprint))
|
|
79
|
-
throw unverified();
|
|
80
67
|
await deps.managedPathsAbsent();
|
|
81
|
-
const bound = { observer_sha256: CLAUDE_POLICY_OBSERVER.sha256, inventory_fingerprint: r.inventoryFingerprint, managed_paths: 'absent' };
|
|
82
|
-
return Object.freeze({ observerSha256: CLAUDE_POLICY_OBSERVER.sha256, inventoryFingerprint: r.inventoryFingerprint, fingerprint: claudeCanonicalSha256(bound) });
|
|
83
68
|
}
|
|
84
69
|
catch {
|
|
85
70
|
throw unverified();
|
|
86
71
|
}
|
|
72
|
+
const bound = { basis: 'managed_paths_as_user', managed_paths: 'absent', directory: CLAUDE_MANAGED_SETTINGS_DIRECTORY,
|
|
73
|
+
files: [...CLAUDE_MANAGED_SETTINGS_FILES], preference_domain: CLAUDE_MANAGED_PREFERENCE_DOMAIN };
|
|
74
|
+
return Object.freeze({ fingerprint: claudeCanonicalSha256(bound) });
|
|
87
75
|
}
|
|
@@ -22,8 +22,6 @@ export const CLAUDE_TEAM_ID = 'Q6L2SF6YDW';
|
|
|
22
22
|
export const CLAUDE_SIGNING_IDENTIFIER = 'com.anthropic.claude-code';
|
|
23
23
|
/** Official native-build distribution. Never taken from the Gateway, a task, config or env. */
|
|
24
24
|
export const CLAUDE_DOWNLOAD_BASE = 'https://downloads.claude.ai/claude-code-releases';
|
|
25
|
-
/** The installed privileged observer; the same reviewed binary the Codex daemon release pins. */
|
|
26
|
-
export const CLAUDE_POLICY_OBSERVER = Object.freeze({ protocol: 'policy-inventory-v1', sha256: '2a07812cd47782826a359b749096b681a6ee49e91f990d48abdc60f2d46e4c9c' });
|
|
27
25
|
/** macOS managed sources (docs/superpowers/specs/2026-10-16-cli-managed-auth-lab-scope.json) and the MDM preference domain. */
|
|
28
26
|
export const CLAUDE_MANAGED_SETTINGS_DIRECTORY = '/Library/Application Support/ClaudeCode';
|
|
29
27
|
export const CLAUDE_MANAGED_SETTINGS_FILES = Object.freeze([`${CLAUDE_MANAGED_SETTINGS_DIRECTORY}/managed-settings.json`, `${CLAUDE_MANAGED_SETTINGS_DIRECTORY}/managed-mcp.json`]);
|