This commit was merged in pull request #845.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { chmod, writeFile } from 'node:fs/promises';
|
||||
import { createConnection, type Socket } from 'node:net';
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 3_000;
|
||||
@@ -185,3 +186,51 @@ export async function requestBrokerReply<T extends object>(
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export interface ReceiptChallengeCycle {
|
||||
sessionId: string;
|
||||
runtimeGeneration: number;
|
||||
receiptChallenge: string;
|
||||
receipt: string;
|
||||
}
|
||||
|
||||
export interface ReceiptChallengeReply {
|
||||
ok: boolean;
|
||||
code?: string;
|
||||
state?: 'UNVERIFIED' | 'PENDING_VERIFICATION' | 'PENDING_PROMOTION' | 'VERIFIED';
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete the shipped begin -> trusted-observer -> consume -> promote path.
|
||||
* The private fixture is read by the daemon's injected test observer; the
|
||||
* observation request itself never carries assistant-message content.
|
||||
*/
|
||||
export async function observeAndPromoteReceiptChallenge(
|
||||
socketPath: string,
|
||||
observerFixturePath: string,
|
||||
cycle: ReceiptChallengeCycle,
|
||||
): Promise<ReceiptChallengeReply> {
|
||||
await writeFile(
|
||||
observerFixturePath,
|
||||
`${JSON.stringify({
|
||||
session_id: cycle.sessionId,
|
||||
runtime_generation: cycle.runtimeGeneration,
|
||||
latest_assistant_message: cycle.receipt,
|
||||
})}\n`,
|
||||
{ encoding: 'utf8', mode: 0o600 },
|
||||
);
|
||||
await chmod(observerFixturePath, 0o600);
|
||||
const observed = await requestBrokerReply<ReceiptChallengeReply>(socketPath, {
|
||||
action: 'observe_receipt',
|
||||
session_id: cycle.sessionId,
|
||||
runtime_generation: cycle.runtimeGeneration,
|
||||
receipt_challenge: cycle.receiptChallenge,
|
||||
});
|
||||
if (observed.ok !== true || observed.state !== 'PENDING_PROMOTION') return observed;
|
||||
return await requestBrokerReply<ReceiptChallengeReply>(socketPath, {
|
||||
action: 'promote_lease',
|
||||
session_id: cycle.sessionId,
|
||||
runtime_generation: cycle.runtimeGeneration,
|
||||
receipt_challenge: cycle.receiptChallenge,
|
||||
});
|
||||
}
|
||||
|
||||
227
packages/mosaic/src/lease-broker/receipt_challenge_unittest.py
Normal file
227
packages/mosaic/src/lease-broker/receipt_challenge_unittest.py
Normal file
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python3
|
||||
"""RED-first contracts for the shipped receipt challenge and observer seam."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import copy
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
TOOLS = Path(__file__).parents[2] / "framework/tools/lease-broker"
|
||||
DAEMON_PATH = TOOLS / "daemon.py"
|
||||
FRAGMENTS_PATH = TOOLS / "normative_fragments.py"
|
||||
OBSERVER_PATH = TOOLS / "receipt_observer.py"
|
||||
|
||||
|
||||
def load_module(name: str, path: Path):
|
||||
assert path.is_file(), f"shipped module is missing: {path}"
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"unable to load {name}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
DAEMON = load_module("lease_broker_receipt_daemon", DAEMON_PATH)
|
||||
FRAGMENTS = load_module("lease_broker_normative_fragments", FRAGMENTS_PATH)
|
||||
|
||||
|
||||
class BrokerFixture(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
root = Path(self.temporary.name)
|
||||
os.chmod(root, 0o700)
|
||||
self.peer = (os.getpid(), os.getuid(), os.getgid())
|
||||
self.broker = DAEMON.Broker(DAEMON.StateStore(root / "state.json"))
|
||||
registered = self.broker.handle(self.peer, {
|
||||
"action": "register_anchor",
|
||||
"runtime_generation": 7,
|
||||
})
|
||||
self.session_id = registered["session_id"]
|
||||
self.assertIsInstance(self.session_id, str)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.temporary.cleanup()
|
||||
|
||||
def construction(self) -> tuple[dict[str, object], dict[str, object]]:
|
||||
content = b"Constitution\n"
|
||||
expected_sha256 = hashlib.sha256(content).hexdigest()
|
||||
construction = {
|
||||
"manifest_version": 1,
|
||||
"generator_version": "wi5-receipt-test",
|
||||
"fragments": [{
|
||||
"source_id": "authority/constitution",
|
||||
"content_base64": base64.b64encode(content).decode("ascii"),
|
||||
"expected_sha256": expected_sha256,
|
||||
}],
|
||||
}
|
||||
result = FRAGMENTS.build_payload(
|
||||
manifest_version=construction["manifest_version"],
|
||||
generator_version=construction["generator_version"],
|
||||
fragments=[FRAGMENTS.NormativeFragment("authority/constitution", content, expected_sha256)],
|
||||
)
|
||||
self.assertEqual(result.injectionDecision, "ACCEPTED")
|
||||
self.assertTrue(result.promotion)
|
||||
return construction, {
|
||||
"compaction_epoch": 3,
|
||||
"request_epoch": 8,
|
||||
"h_source": result.h_source,
|
||||
"h_payload": result.h_payload,
|
||||
"schema_version": 1,
|
||||
}
|
||||
|
||||
def begin(self, binding: dict[str, object], construction: dict[str, object]) -> dict[str, object]:
|
||||
response = self.broker.handle(self.peer, {
|
||||
"action": "begin_verification",
|
||||
"session_id": self.session_id,
|
||||
"runtime_generation": 7,
|
||||
"runtime": "pi",
|
||||
"binding": binding,
|
||||
"construction": construction,
|
||||
})
|
||||
self.assertEqual(response["state"], DAEMON.LEASE_PENDING)
|
||||
self.assertIsInstance(response.get("receipt_challenge"), str)
|
||||
self.assertIsInstance(response.get("receipt"), str)
|
||||
return response
|
||||
|
||||
|
||||
class BuildPayloadAdmissionTest(BrokerFixture):
|
||||
def test_b3_forged_h_source_or_h_payload_is_refused_against_shipped_build_payload(self) -> None:
|
||||
construction, trusted = self.construction()
|
||||
for field in ("h_source", "h_payload"):
|
||||
with self.subTest(field=field):
|
||||
forged = dict(trusted)
|
||||
forged[field] = "f" * 64
|
||||
with self.assertRaisesRegex(DAEMON.BrokerFailure, "PAYLOAD_BINDING_MISMATCH"):
|
||||
self.begin(forged, construction)
|
||||
|
||||
|
||||
class ReceiptObserverTest(BrokerFixture):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
observers = load_module("lease_broker_test_observer", OBSERVER_PATH)
|
||||
self.observer = observers.TestReceiptObserver()
|
||||
self.broker = DAEMON.Broker(self.broker.store, observer=self.observer)
|
||||
|
||||
def record(self, message: str) -> None:
|
||||
self.observer.record_latest_assistant_message(self.session_id, 7, message)
|
||||
|
||||
def observe(self, challenge: str, **untrusted: object) -> dict[str, object]:
|
||||
return self.broker.handle(self.peer, {
|
||||
"action": "observe_receipt",
|
||||
"session_id": self.session_id,
|
||||
"runtime_generation": 7,
|
||||
"receipt_challenge": challenge,
|
||||
**untrusted,
|
||||
})
|
||||
|
||||
def promote(self, challenge: str) -> dict[str, object]:
|
||||
return self.broker.handle(self.peer, {
|
||||
"action": "promote_lease",
|
||||
"session_id": self.session_id,
|
||||
"runtime_generation": 7,
|
||||
"receipt_challenge": challenge,
|
||||
})
|
||||
|
||||
def test_b2_echoed_request_observation_is_refused_but_observer_source_promotes(self) -> None:
|
||||
construction, binding = self.construction()
|
||||
cycle = self.begin(binding, construction)
|
||||
challenge = cycle["receipt_challenge"]
|
||||
receipt = cycle["receipt"]
|
||||
|
||||
with self.assertRaisesRegex(DAEMON.BrokerFailure, "INVALID_RECEIPT"):
|
||||
self.observe(challenge, latest_assistant_message=receipt)
|
||||
with self.assertRaisesRegex(DAEMON.BrokerFailure, "RECEIPT_OBSERVATION_UNAVAILABLE"):
|
||||
self.observe(challenge)
|
||||
|
||||
self.record(receipt)
|
||||
self.assertEqual(self.observe(challenge)["state"], DAEMON.LEASE_PENDING_PROMOTION)
|
||||
self.assertEqual(self.promote(challenge)["state"], DAEMON.LEASE_VERIFIED)
|
||||
self.assertEqual(self.broker.handle(self.peer, {
|
||||
"action": "authorize_tool",
|
||||
"session_id": self.session_id,
|
||||
"runtime_generation": 7,
|
||||
"runtime": "pi",
|
||||
"tool_name": "bash",
|
||||
})["decision"], "allow")
|
||||
|
||||
def test_rejected_begin_keeps_revoke_first_fence_for_all_construction_refusals(self) -> None:
|
||||
construction, binding = self.construction()
|
||||
refusal_cases = {
|
||||
"INVALID_CONSTRUCTION": {"bad": "construction"},
|
||||
"PAYLOAD_CONSTRUCTION_REFUSED": {
|
||||
**construction,
|
||||
"fragments": [{
|
||||
**construction["fragments"][0],
|
||||
"expected_sha256": "0" * 64,
|
||||
}],
|
||||
},
|
||||
"PAYLOAD_BINDING_MISMATCH": None,
|
||||
}
|
||||
for expected_code, rejected_construction in refusal_cases.items():
|
||||
with self.subTest(expected_code=expected_code):
|
||||
verified = self.begin(binding, construction)
|
||||
self.record(verified["receipt"])
|
||||
self.observe(verified["receipt_challenge"])
|
||||
self.assertEqual(self.promote(verified["receipt_challenge"])["state"], DAEMON.LEASE_VERIFIED)
|
||||
|
||||
rejected_binding = copy.deepcopy(binding)
|
||||
if expected_code == "PAYLOAD_BINDING_MISMATCH":
|
||||
rejected_binding["h_payload"] = "f" * 64
|
||||
rejected_construction = construction
|
||||
with self.assertRaisesRegex(DAEMON.BrokerFailure, expected_code):
|
||||
self.begin(rejected_binding, rejected_construction)
|
||||
|
||||
self.assertEqual(
|
||||
self.broker.leases[self.session_id]["state"], DAEMON.LEASE_UNVERIFIED
|
||||
)
|
||||
denied = self.broker.handle(self.peer, {
|
||||
"action": "authorize_tool",
|
||||
"session_id": self.session_id,
|
||||
"runtime_generation": 7,
|
||||
"runtime": "pi",
|
||||
"tool_name": "bash",
|
||||
})
|
||||
self.assertEqual(denied["decision"], "deny")
|
||||
self.assertEqual(denied["state"], DAEMON.LEASE_UNVERIFIED)
|
||||
|
||||
def test_t26_stale_epoch_receipt_cannot_promote_against_shipped_binding(self) -> None:
|
||||
construction, stale_binding = self.construction()
|
||||
stale = self.begin(stale_binding, construction)
|
||||
current_binding = dict(stale_binding)
|
||||
current_binding["compaction_epoch"] = 4
|
||||
current_binding["request_epoch"] = 9
|
||||
current = self.begin(current_binding, construction)
|
||||
self.assertNotEqual(stale["receipt_challenge"], current["receipt_challenge"])
|
||||
|
||||
self.record(stale["receipt"])
|
||||
with self.assertRaisesRegex(DAEMON.BrokerFailure, "RECEIPT_MISMATCH"):
|
||||
self.observe(current["receipt_challenge"])
|
||||
with self.assertRaisesRegex(DAEMON.BrokerFailure, "INVALID_LEASE_TRANSITION"):
|
||||
self.promote(current["receipt_challenge"])
|
||||
|
||||
def test_t29_altered_model_hash_cannot_promote_against_shipped_binding(self) -> None:
|
||||
construction, binding = self.construction()
|
||||
cycle = self.begin(binding, construction)
|
||||
expected = cycle["receipt"]
|
||||
altered_hash = "f" * 64
|
||||
self.assertNotEqual(altered_hash, cycle["binding"]["h_payload"])
|
||||
altered = expected.replace(cycle["binding"]["h_payload"], altered_hash, 1)
|
||||
self.assertNotEqual(altered, expected)
|
||||
|
||||
self.record(altered)
|
||||
with self.assertRaisesRegex(DAEMON.BrokerFailure, "RECEIPT_MISMATCH"):
|
||||
self.observe(cycle["receipt_challenge"])
|
||||
with self.assertRaisesRegex(DAEMON.BrokerFailure, "INVALID_LEASE_TRANSITION"):
|
||||
self.promote(cycle["receipt_challenge"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -6,19 +6,31 @@ import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
|
||||
import { afterEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { launchClaudex, type ClaudexHarnessAdapter } from '../commands/claudex.js';
|
||||
import { requestBrokerReply } from '../lease-broker/broker-test-client.js';
|
||||
import {
|
||||
observeAndPromoteReceiptChallenge,
|
||||
requestBrokerReply,
|
||||
} from '../lease-broker/broker-test-client.js';
|
||||
|
||||
interface BrokerReply {
|
||||
ok: boolean;
|
||||
code?: string;
|
||||
decision?: 'allow' | 'deny';
|
||||
state?: 'UNVERIFIED' | 'PENDING_VERIFICATION' | 'VERIFIED';
|
||||
state?: 'UNVERIFIED' | 'PENDING_VERIFICATION' | 'PENDING_PROMOTION' | 'VERIFIED';
|
||||
session_id?: string;
|
||||
promotion_token?: string;
|
||||
receipt_challenge?: string;
|
||||
receipt?: string;
|
||||
}
|
||||
|
||||
interface PendingReceiptCycle {
|
||||
sessionId: string;
|
||||
runtimeGeneration: number;
|
||||
receiptChallenge: string;
|
||||
receipt: string;
|
||||
}
|
||||
|
||||
interface BrokerPaths {
|
||||
socket: string;
|
||||
observerFixture: string;
|
||||
}
|
||||
|
||||
const frameworkRoot = new URL('../../framework/', import.meta.url).pathname;
|
||||
@@ -38,14 +50,29 @@ const remediationHandlerPath = join(frameworkRoot, 'tools/qa/remediation-hook-ha
|
||||
const children: ChildProcess[] = [];
|
||||
const temporaryRoots: string[] = [];
|
||||
|
||||
const construction = {
|
||||
manifest_version: 1,
|
||||
generator_version: 'mutator-gate-acceptance',
|
||||
fragments: [
|
||||
{
|
||||
source_id: 'authority/mutator-gate-acceptance',
|
||||
content_base64: 'bXV0YXRvci1nYXRlIGFjY2VwdGFuY2UK',
|
||||
expected_sha256: 'cc3de191821d48037f60b4d006fce74b5dd394d39fb5c8bf681c28889ff0e623',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const binding = (compaction_epoch = 1) => ({
|
||||
compaction_epoch,
|
||||
request_epoch: 0,
|
||||
h_source: 'a'.repeat(64),
|
||||
h_payload: 'b'.repeat(64),
|
||||
h_source: '3c8fc6733d6a2bdc001ed9277d636d7cfac037ae48ed7d54203180a3839dc7a6',
|
||||
h_payload: '42da889037c29c3a41397df0f86465ffd121bdb8cd18ad0d7c3df259ab502d3e',
|
||||
schema_version: 1,
|
||||
});
|
||||
|
||||
const pendingReceiptCycles = new Map<string, PendingReceiptCycle>();
|
||||
const observerFixtures = new Map<string, string>();
|
||||
|
||||
async function request(socketPath: string, requestValue: object): Promise<BrokerReply> {
|
||||
return await requestBrokerReply<BrokerReply>(socketPath, requestValue);
|
||||
}
|
||||
@@ -54,9 +81,18 @@ async function startBroker(): Promise<BrokerPaths> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'mosaic-mutator-gate-'));
|
||||
await chmod(root, 0o700);
|
||||
const socket = join(root, 'broker.sock');
|
||||
const observerFixture = join(root, 'test-observer.json');
|
||||
const child = spawn(
|
||||
'python3',
|
||||
[daemonPath, '--socket', socket, '--state', join(root, 'state.json')],
|
||||
[
|
||||
daemonPath,
|
||||
'--socket',
|
||||
socket,
|
||||
'--state',
|
||||
join(root, 'state.json'),
|
||||
'--test-observer-file',
|
||||
observerFixture,
|
||||
],
|
||||
{
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
},
|
||||
@@ -72,7 +108,8 @@ async function startBroker(): Promise<BrokerPaths> {
|
||||
);
|
||||
child.stdout?.once('data', () => resolve());
|
||||
});
|
||||
return { socket };
|
||||
observerFixtures.set(socket, observerFixture);
|
||||
return { socket, observerFixture };
|
||||
}
|
||||
|
||||
interface RuntimeLaunchEntry {
|
||||
@@ -166,28 +203,43 @@ async function beginVerification(
|
||||
ttl_seconds = 300,
|
||||
compactionEpoch = 1,
|
||||
): Promise<BrokerReply> {
|
||||
return await request(socket, {
|
||||
const reply = await request(socket, {
|
||||
action: 'begin_verification',
|
||||
session_id,
|
||||
runtime_generation,
|
||||
runtime,
|
||||
ttl_seconds,
|
||||
binding: binding(compactionEpoch),
|
||||
construction,
|
||||
});
|
||||
if (typeof reply.receipt_challenge === 'string' && typeof reply.receipt === 'string') {
|
||||
pendingReceiptCycles.set(reply.receipt_challenge, {
|
||||
sessionId: session_id,
|
||||
runtimeGeneration: runtime_generation,
|
||||
receiptChallenge: reply.receipt_challenge,
|
||||
receipt: reply.receipt,
|
||||
});
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
async function promote(
|
||||
socket: string,
|
||||
session_id: string,
|
||||
promotion_token: string,
|
||||
receipt_challenge: string,
|
||||
runtime_generation = 1,
|
||||
): Promise<BrokerReply> {
|
||||
return await request(socket, {
|
||||
action: 'promote_lease',
|
||||
session_id,
|
||||
runtime_generation,
|
||||
promotion_token,
|
||||
});
|
||||
const cycle = pendingReceiptCycles.get(receipt_challenge);
|
||||
const observerFixture = observerFixtures.get(socket);
|
||||
if (cycle === undefined || observerFixture === undefined) {
|
||||
return await request(socket, {
|
||||
action: 'promote_lease',
|
||||
session_id,
|
||||
runtime_generation,
|
||||
receipt_challenge,
|
||||
});
|
||||
}
|
||||
return await observeAndPromoteReceiptChallenge(socket, observerFixture, cycle);
|
||||
}
|
||||
|
||||
async function authorize(
|
||||
@@ -249,13 +301,13 @@ describe('whole mutator-class lease gate', () => {
|
||||
|
||||
const pending = await beginVerification(socket, sessionId, 'claude');
|
||||
expect(pending).toMatchObject({ ok: true, state: 'PENDING_VERIFICATION' });
|
||||
expect(pending.promotion_token).toMatch(/^[a-f0-9]{64}$/);
|
||||
expect(pending.receipt_challenge).toMatch(/^[a-f0-9]{64}$/);
|
||||
expect(await authorize(socket, sessionId, 'claude', 'Write')).toMatchObject({
|
||||
ok: false,
|
||||
decision: 'deny',
|
||||
});
|
||||
|
||||
expect(await promote(socket, sessionId, pending.promotion_token!)).toMatchObject({
|
||||
expect(await promote(socket, sessionId, pending.receipt_challenge!)).toMatchObject({
|
||||
ok: true,
|
||||
state: 'VERIFIED',
|
||||
});
|
||||
@@ -271,9 +323,9 @@ describe('whole mutator-class lease gate', () => {
|
||||
ok: false,
|
||||
decision: 'deny',
|
||||
});
|
||||
expect(await promote(socket, sessionId, pending.promotion_token!)).toMatchObject({
|
||||
expect(await promote(socket, sessionId, pending.receipt_challenge!)).toMatchObject({
|
||||
ok: false,
|
||||
code: 'PROMOTION_TOKEN_MISMATCH',
|
||||
code: 'RECEIPT_REPLAY',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -401,7 +453,7 @@ describe('whole mutator-class lease gate', () => {
|
||||
const { socket } = await startBroker();
|
||||
const sessionId = await register(socket);
|
||||
const pending = await beginVerification(socket, sessionId, 'claude', 1, 1);
|
||||
await promote(socket, sessionId, pending.promotion_token!);
|
||||
await promote(socket, sessionId, pending.receipt_challenge!);
|
||||
|
||||
// Intentionally invoke neither compaction observer: this is the amended
|
||||
// D2-v5 bounded residual, not a fail-closed path.
|
||||
@@ -431,7 +483,7 @@ describe('whole mutator-class lease gate', () => {
|
||||
const { socket } = await startBroker();
|
||||
const sessionId = await register(socket);
|
||||
const pending = await beginVerification(socket, sessionId, 'claude');
|
||||
await promote(socket, sessionId, pending.promotion_token!);
|
||||
await promote(socket, sessionId, pending.receipt_challenge!);
|
||||
|
||||
const revoked = spawnSync('python3', [revokerPath, '--runtime', 'claude', '--reason', reason], {
|
||||
encoding: 'utf8',
|
||||
@@ -454,7 +506,7 @@ describe('whole mutator-class lease gate', () => {
|
||||
const { socket } = await startBroker();
|
||||
const observerSessionId = await register(socket);
|
||||
const observerPending = await beginVerification(socket, observerSessionId, 'claude');
|
||||
await promote(socket, observerSessionId, observerPending.promotion_token!);
|
||||
await promote(socket, observerSessionId, observerPending.receipt_challenge!);
|
||||
|
||||
expect(await authorize(socket, observerSessionId, 'claude', 'Bash')).toMatchObject({
|
||||
ok: true,
|
||||
@@ -464,12 +516,10 @@ describe('whole mutator-class lease gate', () => {
|
||||
const retriedPromotion = await promote(
|
||||
socket,
|
||||
observerSessionId,
|
||||
observerPending.promotion_token!,
|
||||
observerPending.receipt_challenge!,
|
||||
);
|
||||
expect(retriedPromotion.ok).toBe(false);
|
||||
expect(['PROMOTION_TOKEN_MISMATCH', 'INVALID_LEASE_TRANSITION']).toContain(
|
||||
retriedPromotion.code,
|
||||
);
|
||||
expect(retriedPromotion.code).toBe('RECEIPT_REPLAY');
|
||||
|
||||
const revoked = spawnSync(
|
||||
'python3',
|
||||
@@ -495,7 +545,7 @@ describe('whole mutator-class lease gate', () => {
|
||||
const expirySessionId = await register(expirySocket);
|
||||
expect(expirySessionId).not.toBe(observerSessionId);
|
||||
const expiryPending = await beginVerification(expirySocket, expirySessionId, 'claude', 1, 1);
|
||||
await promote(expirySocket, expirySessionId, expiryPending.promotion_token!);
|
||||
await promote(expirySocket, expirySessionId, expiryPending.receipt_challenge!);
|
||||
expect(await authorize(expirySocket, expirySessionId, 'claude', 'Bash')).toMatchObject({
|
||||
ok: true,
|
||||
decision: 'allow',
|
||||
@@ -514,7 +564,7 @@ describe('whole mutator-class lease gate', () => {
|
||||
const { socket } = await startBroker();
|
||||
const sessionId = await register(socket);
|
||||
const pending = await beginVerification(socket, sessionId, 'claude');
|
||||
await promote(socket, sessionId, pending.promotion_token!);
|
||||
await promote(socket, sessionId, pending.receipt_challenge!);
|
||||
|
||||
const root = await mkdtemp(join(tmpdir(), 'mosaic-observer-fence-'));
|
||||
temporaryRoots.push(root);
|
||||
@@ -547,7 +597,7 @@ describe('whole mutator-class lease gate', () => {
|
||||
const { socket } = await startBroker();
|
||||
const sessionId = await register(socket);
|
||||
const pending = await beginVerification(socket, sessionId, 'pi');
|
||||
await promote(socket, sessionId, pending.promotion_token!);
|
||||
await promote(socket, sessionId, pending.receipt_challenge!);
|
||||
const anchorPid = process.pid;
|
||||
|
||||
const root = await mkdtemp(join(tmpdir(), 'mosaic-generation-bump-'));
|
||||
@@ -612,7 +662,7 @@ describe('whole mutator-class lease gate', () => {
|
||||
const { socket } = await startBroker();
|
||||
const sessionId = await register(socket);
|
||||
const pending = await beginVerification(socket, sessionId, 'claude', 1, 1);
|
||||
await promote(socket, sessionId, pending.promotion_token!);
|
||||
await promote(socket, sessionId, pending.receipt_challenge!);
|
||||
|
||||
expect(await authorize(socket, sessionId, 'claude', 'Bash')).toMatchObject({
|
||||
ok: true,
|
||||
@@ -626,7 +676,7 @@ describe('whole mutator-class lease gate', () => {
|
||||
});
|
||||
|
||||
const refreshed = await beginVerification(socket, sessionId, 'claude', 1, 300, 2);
|
||||
await promote(socket, sessionId, refreshed.promotion_token!);
|
||||
await promote(socket, sessionId, refreshed.receipt_challenge!);
|
||||
expect(
|
||||
await request(socket, {
|
||||
action: 'revoke_lease',
|
||||
@@ -646,7 +696,7 @@ describe('whole mutator-class lease gate', () => {
|
||||
const { socket } = await startBroker();
|
||||
const sessionId = await register(socket);
|
||||
const pending = await beginVerification(socket, sessionId, 'pi');
|
||||
await promote(socket, sessionId, pending.promotion_token!);
|
||||
await promote(socket, sessionId, pending.receipt_challenge!);
|
||||
|
||||
expect(await authorize(socket, sessionId, 'pi', 'bash', 2)).toMatchObject({
|
||||
ok: false,
|
||||
@@ -859,7 +909,7 @@ raise SystemExit(0 if len(session_id) == 64 and hook_present and observers_prese
|
||||
expect(runRuntimeGate(socket, sessionId, 'pi', 'unknown_custom_tool').status).toBe(2);
|
||||
|
||||
const pending = await beginVerification(socket, sessionId, 'claude');
|
||||
await promote(socket, sessionId, pending.promotion_token!);
|
||||
await promote(socket, sessionId, pending.receipt_challenge!);
|
||||
expect(runRuntimeGate(socket, sessionId, 'claude', 'Bash').status).toBe(0);
|
||||
|
||||
const settings = JSON.parse(await readFile(claudeSettingsPath, 'utf8')) as {
|
||||
|
||||
Reference in New Issue
Block a user