@topolo/cli 0.13.10 → 0.13.12
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 +22 -0
- package/agent-packs/README.md +53 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +14 -2
- package/agent-packs/src/topolo_agent_pack/lanes.py +158 -5
- package/agent-packs/src/topolo_agent_pack/live.py +421 -2
- package/agent-packs/src/topolo_agent_pack/topolo.py +166 -5
- package/dist/{claude-pack-backend-RMSOPHVQ.js → claude-pack-backend-YEVPAOID.js} +76 -31
- package/dist/index.js +549 -132
- package/package.json +5 -5
|
@@ -5,22 +5,60 @@ from .storage import PackError,atomic_json,canonical,read_json,safe_id
|
|
|
5
5
|
class ConnectionSourceRefused(PackError):
|
|
6
6
|
pass
|
|
7
7
|
|
|
8
|
+
def managed_runtime_selection(path):
|
|
9
|
+
"""Configuration custody only, never a health or native permission ACK."""
|
|
10
|
+
from . import live
|
|
11
|
+
before,raw=live.adoption_snapshot(path)
|
|
12
|
+
live.adoption_require(before['mode']==0o600 and len(raw)<=2*1024*1024,'private runtime descriptor required')
|
|
13
|
+
value=json.loads(raw)
|
|
14
|
+
live.adoption_keys(value,['schemaVersion','library','ownerGeneration','profile','runtime','preserve'])
|
|
15
|
+
live.adoption_require(value['schemaVersion']==1 and type(value['schemaVersion']) is int and
|
|
16
|
+
pathlib.Path(path).parent==pathlib.Path(value['library'])/'launchers','owned runtime descriptor location required')
|
|
17
|
+
live.adoption_read_inputs(value)
|
|
18
|
+
live.adoption_pin(value['ownerGeneration'],value['profile'])
|
|
19
|
+
env=live.adoption_runtime(value['runtime'],value['profile'])
|
|
20
|
+
for pin in value['preserve']:live.adoption_pin(pin,value['profile'])
|
|
21
|
+
live.adoption_require(live.adoption_snapshot(path,profile=value['profile'])[0]==before,'runtime descriptor changed')
|
|
22
|
+
return value,env
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def managed_mcp_main(path,args=()):
|
|
26
|
+
from .live import adoption_require
|
|
27
|
+
adoption_require(len(args)<=1 and all(isinstance(v,str) and re.fullmatch(r'[A-Za-z0-9_-]+',v) for v in args),'closed MCP agent name required')
|
|
28
|
+
value,env=managed_runtime_selection(path)
|
|
29
|
+
if args:env['TOPOLO_AGENT_NAME']=args[0]
|
|
30
|
+
runtime=value['runtime']
|
|
31
|
+
# Only a later owner-admitted ordinary native startup invokes this wrapper.
|
|
32
|
+
# Credential resolution/refresh/identity are real private effects, not preflight.
|
|
33
|
+
# Replace this wrapper, retaining the native host's PID/stdin/cancellation
|
|
34
|
+
# custody instead of leaving an intermediate parent that can orphan MCP.
|
|
35
|
+
return os.execve(runtime['node']['path'],[runtime['node']['path'],runtime['mcp']['path']],env)
|
|
36
|
+
|
|
37
|
+
|
|
8
38
|
class Client:
|
|
9
|
-
def __init__(self,environment,context,binary=None):
|
|
39
|
+
def __init__(self,environment,context,binary=None,env=None,node=None):
|
|
10
40
|
if environment not in {'development','staging','production'}:
|
|
11
41
|
raise PackError('Explicit Topolo environment required')
|
|
12
42
|
if not context or any(c in context for c in '\n\r'):
|
|
13
43
|
raise PackError('Explicit Topolo context required')
|
|
14
|
-
|
|
44
|
+
selected=os.environ if env is None else env
|
|
45
|
+
descriptor=selected.get('TOPOLO_AGENT_PACK_RUNTIME')
|
|
46
|
+
if descriptor:
|
|
47
|
+
value,selected=managed_runtime_selection(descriptor)
|
|
48
|
+
if (environment!=value['profile']['environment'] or context!=value['profile']['contextKey'] or binary is not None or node is not None):
|
|
49
|
+
raise ConnectionSourceRefused('Managed private runtime tuple or explicit override differs')
|
|
50
|
+
binary=value['runtime']['cli']['path'];node=value['runtime']['node']['path']
|
|
51
|
+
if any(selected.get(k) for k in ['TOPOLO_API_KEY','TOPOLO_ACCESS_TOKEN','TOPOLO_'+environment.upper()+'_API_KEY','TOPOLO_'+environment.upper()+'_ACCESS_TOKEN']):
|
|
15
52
|
raise PackError('Pinned connections require the CLI protected account context, not injected credentials')
|
|
16
53
|
self.environment=environment;self.context=context
|
|
17
54
|
self.binary=binary or shutil.which('topolo')
|
|
18
55
|
if not self.binary:
|
|
19
56
|
raise PackError('Install the public @topolo/cli and authenticate before connecting')
|
|
20
|
-
self.env={**
|
|
21
|
-
self.
|
|
57
|
+
self.env={**selected,'TOPOLO_CONTEXT':context}
|
|
58
|
+
self.command=[node,self.binary] if node else [self.binary]
|
|
59
|
+
self.env['PATH']=(str(pathlib.Path(node).parent)+os.pathsep+'/usr/bin:/bin') if node else str(pathlib.Path(self.binary).parent)+os.pathsep+self.env.get('PATH','')
|
|
22
60
|
def run(self,args):
|
|
23
|
-
p=subprocess.run([self.
|
|
61
|
+
p=subprocess.run([*self.command,'--env',self.environment,*args,'--json'],env=self.env,capture_output=True,text=True,timeout=90)
|
|
24
62
|
if p.returncode:
|
|
25
63
|
raise PackError('Topolo command failed (exit '+str(p.returncode)+'); inspect the published action, account permissions and environment. No fallback was used.')
|
|
26
64
|
try:value=json.loads(p.stdout)
|
|
@@ -599,6 +637,8 @@ def connection_lock(lib,args):
|
|
|
599
637
|
yield
|
|
600
638
|
|
|
601
639
|
def dispatch(lib,args):
|
|
640
|
+
if args.command=='remote-context' and getattr(args,'continuity_input',None):
|
|
641
|
+
return native_cli_continuity_read(lib,args)
|
|
602
642
|
client=Client(args.environment,args.context_key)
|
|
603
643
|
if args.command=='connect':return connect(lib,args,client)
|
|
604
644
|
if args.command=='remote-context':return remote_context(lib,args,client)[1]
|
|
@@ -610,3 +650,124 @@ def dispatch(lib,args):
|
|
|
610
650
|
reference=read_json(args.reference)
|
|
611
651
|
return read_artifact(client,binding,reference['key'],reference,args.destination)
|
|
612
652
|
raise PackError('Unsupported Topolo command')
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def native_cli_response(stdout,stderr,service):
|
|
657
|
+
"""Preserve only canonical public response metadata; never echo raw stderr."""
|
|
658
|
+
from . import lanes
|
|
659
|
+
if len(stdout.encode())>262144 or len(stderr.encode())>65536:
|
|
660
|
+
raise PackError('native_cli_request_metadata_unavailable: bounded public response required')
|
|
661
|
+
try:
|
|
662
|
+
body=json.loads(stdout)
|
|
663
|
+
except ValueError as error:
|
|
664
|
+
raise PackError('native_cli_request_metadata_unavailable: invalid public JSON') from error
|
|
665
|
+
if not isinstance(body,dict) or body.get('error'):
|
|
666
|
+
raise PackError('native_cli_request_metadata_unavailable: unsuccessful public response')
|
|
667
|
+
events=[];seen=set()
|
|
668
|
+
pattern=r'^\[topolo\] ← (\d{3}) (GET|POST) (auth|agent) (/[^\s?#\\]+) \([0-9.]+ms, req=([A-Za-z0-9][A-Za-z0-9._:-]{0,127})\)$'
|
|
669
|
+
for line in stderr.splitlines():
|
|
670
|
+
match=re.fullmatch(pattern,line)
|
|
671
|
+
if not match:
|
|
672
|
+
continue
|
|
673
|
+
status,method,target,path,request_id=match.groups()
|
|
674
|
+
if request_id=='unavailable' or request_id in seen or not 200<=int(status)<300:
|
|
675
|
+
raise PackError('native_cli_request_metadata_unavailable: ambiguous or unsuccessful response request IDs')
|
|
676
|
+
seen.add(request_id);events.append({'requestId':request_id,'method':method,'service':target,'path':path,'status':int(status)})
|
|
677
|
+
if not 1<=len(events)<=8 or service not in {event['service'] for event in events}:
|
|
678
|
+
raise PackError('native_cli_request_metadata_unavailable: actual canonical server response IDs required')
|
|
679
|
+
return body.get('data',body),events
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def native_cli_command_response(client,args,service):
|
|
683
|
+
env={**client.env,'TOPOLO_DEBUG':'1'}
|
|
684
|
+
result=subprocess.run([*client.command,'--env',client.environment,*args,'--json'],env=env,
|
|
685
|
+
capture_output=True,text=True,timeout=90)
|
|
686
|
+
if result.returncode:
|
|
687
|
+
raise PackError('native_cli_request_metadata_unavailable: public command refused; no fallback or retry')
|
|
688
|
+
return native_cli_response(result.stdout,result.stderr,service)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def native_cli_selected_environment(value,binding,host_env):
|
|
692
|
+
from . import lanes
|
|
693
|
+
from types import SimpleNamespace
|
|
694
|
+
context=value['context'];profile=binding.get('privateCredentialProfile')
|
|
695
|
+
lanes.native_cli_closed(profile,{'format','schemaVersion','configPath','environment','contextKey','identity'},'private profile reference')
|
|
696
|
+
if (profile['format']!='topolo-private-credential-profile' or profile['schemaVersion']!=1 or
|
|
697
|
+
profile['environment']!=context['environment'] or profile['contextKey']!=context['contextKey'] or
|
|
698
|
+
profile['identity']!={'userId':context['principalId'],'organizationId':context['organizationId']}):
|
|
699
|
+
raise PackError('Exact native-cli approved profile/principal/context reference required')
|
|
700
|
+
selected=SimpleNamespace(env={'TOPOLO_CONFIG_PATH':profile['configPath']},environment=context['environment'],context=context['contextKey'])
|
|
701
|
+
args=SimpleNamespace(private_profile=True,environment=context['environment'],context_key=context['contextKey'])
|
|
702
|
+
private_profile_reference(selected,args)
|
|
703
|
+
for path,sha in [(value['cli']['nodeExecutable'],value['cli']['nodeDigest']),(value['cli']['entry'],value['cli']['entryDigest'])]:
|
|
704
|
+
p=pathlib.Path(path);info=p.lstat()
|
|
705
|
+
if (p.resolve()!=p or not stat.S_ISREG(info.st_mode) or info.st_uid not in {0,os.getuid()} or info.st_mode&0o022 or
|
|
706
|
+
not info.st_mode&0o111 or hashlib.sha256(p.read_bytes()).hexdigest()!=sha):
|
|
707
|
+
raise PackError('Native-cli executable source/provenance changed')
|
|
708
|
+
allowed={'HOME','USER','LOGNAME','PATH','LANG','LC_ALL','TMPDIR'}
|
|
709
|
+
env={k:v for k,v in host_env.items() if k in allowed and isinstance(v,str)}
|
|
710
|
+
env['PATH']=str(pathlib.Path(value['cli']['nodeExecutable']).parent)+os.pathsep+'/usr/bin:/bin'
|
|
711
|
+
env.update(TOPOLO_CONFIG_PATH=profile['configPath'],TOPOLO_ENV=context['environment'],TOPOLO_CONTEXT=context['contextKey'])
|
|
712
|
+
return env
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def native_cli_continuity_read(lib,args,*,_admit=None,_client_factory=None,_response=None):
|
|
716
|
+
"""Native command producer. No current runtime authority producer exists.
|
|
717
|
+
|
|
718
|
+
Injected callbacks are internal hermetic seams, never CLI/JSON options.
|
|
719
|
+
Receipt output is public response evidence, not native-query attribution.
|
|
720
|
+
"""
|
|
721
|
+
from . import lanes
|
|
722
|
+
path=pathlib.Path(args.continuity_input);info=path.lstat()
|
|
723
|
+
if (path.is_symlink() or not stat.S_ISREG(info.st_mode) or info.st_uid!=os.getuid() or info.st_nlink!=1 or
|
|
724
|
+
info.st_mode&0o077 or info.st_size>65536):
|
|
725
|
+
raise PackError('Exact owned bounded native-cli input required')
|
|
726
|
+
value=lanes.native_cli_input(read_json(path))
|
|
727
|
+
(lanes.native_cli_admission if _admit is None else _admit)(value)
|
|
728
|
+
context=value['context']
|
|
729
|
+
if (args.environment!=context['environment'] or args.context_key!=context['contextKey'] or args.role!=context['logicalId'] or
|
|
730
|
+
value['sourceRoleKey']!=args.name+'/'+args.role):
|
|
731
|
+
raise PackError('Native-cli command differs from the exact source/operation')
|
|
732
|
+
file=binding_path(lib,args.name,args.environment,args.context_key);info=file.lstat()
|
|
733
|
+
if (file.is_symlink() or not stat.S_ISREG(info.st_mode) or info.st_uid!=os.getuid() or info.st_nlink!=1 or
|
|
734
|
+
hashlib.sha256(file.read_bytes()).hexdigest()!=value['source']['bindingDigest']):
|
|
735
|
+
raise PackError('Native-cli current owned binding source differs')
|
|
736
|
+
binding=read_json(file);roles=[r for r in binding['input']['roles'] if r['logicalId']==args.role]
|
|
737
|
+
if (len(roles)!=1 or binding['identity']!={'userId':context['principalId'],'organizationId':context['organizationId']} or
|
|
738
|
+
binding['packRevision']!=value['source']['packRevision'] or binding['input']['instanceId']!=context['installationId'] or
|
|
739
|
+
binding['agentWorkspaceId']!=context['agentWorkspaceId'] or binding['input']['bytesWorkspaceId']!=context['bytesWorkspaceId'] or
|
|
740
|
+
binding['input']['bytesScope']!=context['bytesScope'] or roles[0]['agentId']!=context['agentId'] or roles[0]['agentVersionId']!=context['agentVersionId']):
|
|
741
|
+
raise PackError('Native-cli source principal/Agent/Bytes binding differs')
|
|
742
|
+
env=native_cli_selected_environment(value,binding,os.environ)
|
|
743
|
+
client=(Client if _client_factory is None else _client_factory)(args.environment,args.context_key,
|
|
744
|
+
binary=value['cli']['entry'],env=env,node=value['cli']['nodeExecutable'])
|
|
745
|
+
call=native_cli_command_response if _response is None else _response
|
|
746
|
+
identity,identity_events=call(client,['whoami'],'auth')
|
|
747
|
+
if (identity.get('user',{}).get('id')!=context['principalId'] or (identity.get('organization')or{}).get('id')!=context['organizationId']):
|
|
748
|
+
raise PackError('Actual native-cli public identity differs')
|
|
749
|
+
with tempfile.TemporaryDirectory(prefix='topolo-native-cli-context-') as directory:
|
|
750
|
+
input_file=pathlib.Path(directory)/'input.json';atomic_json(input_file,{'instanceId':context['installationId'],'logicalId':context['logicalId']})
|
|
751
|
+
envelope,context_events=call(client,['actions','call','app_topolo_agent.agent_packs.context','--detail','--data','@'+str(input_file),
|
|
752
|
+
'--resource-type','workspace','--resource-id',context['agentWorkspaceId']],'agent')
|
|
753
|
+
result=envelope.get('data',envelope)
|
|
754
|
+
actual=result.get('context',{})
|
|
755
|
+
expected={'organizationId':context['organizationId'],'workspaceId':context['agentWorkspaceId'],'instanceId':context['installationId'],
|
|
756
|
+
'logicalId':context['logicalId'],'agentId':context['agentId'],'agentVersionId':context['agentVersionId'],
|
|
757
|
+
'artifacts':{'appId':'app_topolo_bytes','resourceType':'workspace','resourceId':context['bytesWorkspaceId'],'scope':context['bytesScope']}}
|
|
758
|
+
if envelope.get('success') is False or any(actual.get(k)!=v for k,v in expected.items()):
|
|
759
|
+
raise PackError('Actual native-cli public Agent/Bytes context differs')
|
|
760
|
+
# Recheck mutable public binding, binary and stat-only profile after awaited reads.
|
|
761
|
+
if hashlib.sha256(file.read_bytes()).hexdigest()!=value['source']['bindingDigest']:
|
|
762
|
+
raise PackError('Native-cli binding changed during response; no receipt ACK')
|
|
763
|
+
native_cli_selected_environment(value,binding,os.environ)
|
|
764
|
+
(lanes.native_cli_admission if _admit is None else _admit)(value)
|
|
765
|
+
responses={}
|
|
766
|
+
for kind,body,events in [('identity',{'userId':context['principalId'],'organizationId':context['organizationId']},identity_events),
|
|
767
|
+
('context',expected,context_events)]:
|
|
768
|
+
responses[kind]={'requestEvents':events,'bodyProjectionDigest':lanes.digest(body),'actionId':None if kind=='identity' else 'app_topolo_agent.agent_packs.context',
|
|
769
|
+
'resourceType':None if kind=='identity' else 'workspace','resourceId':None if kind=='identity' else context['agentWorkspaceId']}
|
|
770
|
+
receipt={k:value[k] for k in lanes.NATIVE_CLI_RECEIPT_FIELDS-{'format','schemaVersion','responses','claim'}}
|
|
771
|
+
receipt.update(format='topolo-native-cli-continuity-receipt',schemaVersion=1,responses=responses,
|
|
772
|
+
claim='PUBLIC_IDENTITY_CONTEXT_RESPONSE_ONLY_NATIVE_ATTRIBUTION_REQUIRED')
|
|
773
|
+
return lanes.native_cli_receipt(value,receipt)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/lib/claude-pack-backend.ts
|
|
4
|
-
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
4
|
+
import { createHash as createHash2, randomBytes, randomUUID } from "crypto";
|
|
5
5
|
import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
6
6
|
import * as filesystem from "fs";
|
|
7
7
|
import * as cryptography from "crypto";
|
|
@@ -13,6 +13,7 @@ import { createConnection } from "net";
|
|
|
13
13
|
import { WebSocket } from "undici";
|
|
14
14
|
|
|
15
15
|
// src/lib/claude-native-pack.ts
|
|
16
|
+
import { createHash } from "crypto";
|
|
16
17
|
async function nativeClaudePackOperation(manager, sidebar, plan, apply, checkpoint, verifyContext = () => {
|
|
17
18
|
}) {
|
|
18
19
|
const fenceKey = /* @__PURE__ */ Symbol.for("topolo.agent-pack.native-operation.v1");
|
|
@@ -849,7 +850,7 @@ async function nativeClaudeApprovalOperation(manager, routes, operation, fingerp
|
|
|
849
850
|
const session = await manager.getSession(route.sessionId);
|
|
850
851
|
if (!session || session.sessionId !== route.sessionId || session.cwd !== route.cwd || session.title !== route.title || session.isArchived)
|
|
851
852
|
throw new Error("Native approval session identity differs: " + route.key);
|
|
852
|
-
if (!descriptor || descriptor.sessionId !== route.sessionId || descriptor.cwd !== route.cwd || descriptor.title !== route.title || descriptor.isArchived || descriptor.backend?.kind !== "local" || descriptor.backend.remoteTarget || descriptor.worktreePath || descriptor.worktreeLazy || (route.dynamic ? descriptor.spawnedFrom?.sessionId !== route.dynamic.nativeParentId || descriptor.spawnedFrom?.sideSession !== true || route.dynamic.nativeParentId !== route.intakeSessionId || !/^[a-f0-9]{64}$/.test(route.dynamic.planDigest) : descriptor.spawnedFrom))
|
|
853
|
+
if (!descriptor || descriptor.sessionId !== route.sessionId || descriptor.cwd !== route.cwd || descriptor.title !== route.title || descriptor.isArchived || descriptor.backend?.kind !== "local" || descriptor.backend.remoteTarget || descriptor.worktreePath || descriptor.worktreeLazy || (route.dynamic ? (route.dynamic.continuityTransport === "native-cli" ? Boolean(descriptor.spawnedFrom) || route.dynamic.nativeParentId !== null : descriptor.spawnedFrom?.sessionId !== route.dynamic.nativeParentId || descriptor.spawnedFrom?.sideSession !== true || route.dynamic.nativeParentId !== route.intakeSessionId) || !/^[a-f0-9]{64}$/.test(route.dynamic.planDigest) : descriptor.spawnedFrom))
|
|
853
854
|
throw new Error("Native approval session descriptor identity differs: " + route.key);
|
|
854
855
|
return session;
|
|
855
856
|
};
|
|
@@ -993,11 +994,30 @@ async function nativeClaudeApprovalOperation(manager, routes, operation, fingerp
|
|
|
993
994
|
if (operation.kind === "list") return { requests: [...state.entries.values()].map((e) => ({ ...e.record })) };
|
|
994
995
|
const entry = state.entries.get(operation.key);
|
|
995
996
|
if (!entry || entry.record.state !== "pending" || entry.record.intakeSessionId !== operation.intake || entry.record.fingerprint !== operation.fingerprint) throw new Error("Exact pending request and owning intake required.");
|
|
996
|
-
|
|
997
|
-
|
|
997
|
+
let cliFinalGuard;
|
|
998
|
+
if (operation.kind === "respond" && operation.decision === "once" && byId.get(entry.record.sourceSessionId)?.dynamic) {
|
|
999
|
+
const route = byId.get(entry.record.sourceSessionId);
|
|
1000
|
+
if (route.dynamic?.continuityTransport === "native-cli") {
|
|
1001
|
+
const producer = state.dynamicLifecycle?.get(entry.record.sourceSessionId)?.authorizeNativeCLI;
|
|
1002
|
+
if (typeof producer !== "function") throw Object.assign(new Error("native_cli_startup_unqualified: actual native CLI admission producer unavailable."), { code: "native_cli_startup_unqualified" });
|
|
1003
|
+
const admission = await producer(entry.record, entry.request, operation);
|
|
1004
|
+
if (!admission || !["bootstrap", "work"].includes(admission.phase) || admission.nativeId !== entry.record.sourceSessionId || admission.intakeNativeId !== entry.record.intakeSessionId || admission.fingerprint !== entry.record.fingerprint || admission.generation !== route.dynamic.generation || typeof route.dynamic.generation !== "string" || !route.dynamic.generation || !admission.query || typeof admission.verifyCurrent !== "function" || typeof admission.model !== "string" || !admission.model || typeof admission.effort !== "string" || !admission.effort)
|
|
1005
|
+
throw new Error("Exact current native CLI query/intake admission differs.");
|
|
1006
|
+
const raw = await manager.getSessionWhenLoaded(entry.record.sourceSessionId, "topolo-native-cli-permission", { warnOnMiss: false });
|
|
1007
|
+
if (raw?.query !== admission.query || typeof manager.readRunningModel !== "function" || typeof manager.getEffort !== "function" || await manager.readRunningModel(raw, raw.query) !== admission.model || await manager.getEffort(raw.sessionId) !== admission.effort)
|
|
1008
|
+
throw new Error("Actual same-query native model/effort acknowledgement differs.");
|
|
1009
|
+
cliFinalGuard = () => {
|
|
1010
|
+
if (state.dynamicLifecycle?.get(entry.record.sourceSessionId)?.authorizeNativeCLI !== producer || raw.query !== admission.query || admission.verifyCurrent() !== void 0) throw new Error("Current native CLI admission changed before dispatch.");
|
|
1011
|
+
};
|
|
1012
|
+
if (admission.phase === "bootstrap") {
|
|
1013
|
+
if (entry.request.toolName !== "Bash" || entry.request.input?.command !== admission.command || admission.commandDigest !== fingerprint(admission.command))
|
|
1014
|
+
throw new Error("Only the exact native CLI bootstrap command may precede context acknowledgement.");
|
|
1015
|
+
} else if (!/^[a-f0-9]{64}$/.test(admission.contextReceiptDigest ?? "")) throw new Error("Native CLI work requires verified same-query context receipt.");
|
|
1016
|
+
} else throw Object.assign(
|
|
998
1017
|
new Error("native_private_query_prestart_contract_unqualified: dynamic private query authorization remains closed."),
|
|
999
1018
|
{ code: "native_private_query_prestart_contract_unqualified" }
|
|
1000
1019
|
);
|
|
1020
|
+
}
|
|
1001
1021
|
const pending = await current(entry);
|
|
1002
1022
|
if (manager[symbol] !== state || state.entries.get(operation.key) !== entry || entry.record.state !== "pending" || state.auditFailed)
|
|
1003
1023
|
throw new Error("Exact pending request changed during native identity validation; reconcile without replay.");
|
|
@@ -1005,6 +1025,7 @@ async function nativeClaudeApprovalOperation(manager, routes, operation, fingerp
|
|
|
1005
1025
|
if (operation.kind !== "respond" || operation.humanDecisionRecorded !== true || !["once", "deny"].includes(operation.decision ?? ""))
|
|
1006
1026
|
throw new Error("Recorded human one-request decision required; never auto-approve.");
|
|
1007
1027
|
if (operation.decision !== "deny" && entry.record.cardOnly) throw new Error("Claude requires the original card; intake cannot impersonate its origin.");
|
|
1028
|
+
cliFinalGuard?.();
|
|
1008
1029
|
entry.record.state = "response-may-have-effects";
|
|
1009
1030
|
checkpoint({ ...entry.record });
|
|
1010
1031
|
await manager.respondToToolPermission(pending.requestId, operation.decision, void 0, "elsewhere");
|
|
@@ -1019,17 +1040,26 @@ async function nativeClaudeApprovalOperation(manager, routes, operation, fingerp
|
|
|
1019
1040
|
}
|
|
1020
1041
|
async function nativeClaudeAppendDynamicRoute(manager, route, verify, checkpoint, lifecycle) {
|
|
1021
1042
|
const state = manager[/* @__PURE__ */ Symbol.for("topolo.agent-pack.approvals.v1")];
|
|
1022
|
-
if (!state || state.auditFailed || !state.byId || !state.byKey || !route.dynamic || !/^[a-f0-9]{64}$/.test(route.dynamic.planDigest) || route.dynamic.nativeParentId !== route.intakeSessionId)
|
|
1043
|
+
if (!state || state.auditFailed || !state.byId || !state.byKey || !route.dynamic || !/^[a-f0-9]{64}$/.test(route.dynamic.planDigest) || (route.dynamic.continuityTransport === "native-cli" ? route.dynamic.nativeParentId !== null : route.dynamic.nativeParentId !== route.intakeSessionId))
|
|
1023
1044
|
throw new Error("Current qualified original approval listener required.");
|
|
1045
|
+
let cliAdmission;
|
|
1046
|
+
if (route.dynamic.continuityTransport === "native-cli") {
|
|
1047
|
+
if (typeof lifecycle?.authorizeNativeCLI !== "function") throw Object.assign(new Error("native_cli_startup_unqualified: actual native CLI admission producer unavailable."), { code: "native_cli_startup_unqualified" });
|
|
1048
|
+
cliAdmission = await lifecycle.authorizeNativeCLI(null, null, { kind: "route-append" });
|
|
1049
|
+
}
|
|
1024
1050
|
const intake = state.byId.get(route.intakeSessionId);
|
|
1025
1051
|
if (!intake || intake.key !== route.intakeKey || intake.contextKey !== route.contextKey || !intake.key.includes("/intakes/"))
|
|
1026
1052
|
throw new Error("Dynamic approval route has no exact same-context original intake.");
|
|
1027
1053
|
await verify();
|
|
1028
1054
|
const raw = await manager.getSessionWhenLoaded(route.sessionId, "topolo-dynamic-route", { warnOnMiss: false });
|
|
1029
1055
|
const dto = await manager.getSession(route.sessionId);
|
|
1030
|
-
if (!raw || !dto || raw.sessionId !== route.sessionId || raw.cwd !== route.cwd || raw.title !== route.title || dto.sessionId !== route.sessionId || dto.cwd !== route.cwd || dto.title !== route.title || raw.isArchived || dto.isArchived || raw.backend?.kind !== "local" || raw.backend.remoteTarget || raw.worktreePath || raw.worktreeLazy || raw.spawnedFrom?.sessionId !== route.intakeSessionId || raw.spawnedFrom?.sideSession !== true)
|
|
1056
|
+
if (!raw || !dto || raw.sessionId !== route.sessionId || raw.cwd !== route.cwd || raw.title !== route.title || dto.sessionId !== route.sessionId || dto.cwd !== route.cwd || dto.title !== route.title || raw.isArchived || dto.isArchived || raw.backend?.kind !== "local" || raw.backend.remoteTarget || raw.worktreePath || raw.worktreeLazy || (route.dynamic.continuityTransport === "native-cli" ? Boolean(raw.spawnedFrom) : raw.spawnedFrom?.sessionId !== route.intakeSessionId || raw.spawnedFrom?.sideSession !== true))
|
|
1031
1057
|
throw new Error("Actual dynamic child descriptor differs before route append.");
|
|
1058
|
+
if (route.dynamic.continuityTransport === "native-cli" && (!cliAdmission?.query || raw.query !== cliAdmission.query || cliAdmission.nativeId !== route.sessionId || cliAdmission.intakeNativeId !== route.intakeSessionId || cliAdmission.generation !== route.dynamic.generation || typeof route.dynamic.generation !== "string" || !route.dynamic.generation || typeof cliAdmission.verifyCurrent !== "function" || typeof cliAdmission.model !== "string" || !cliAdmission.model || typeof cliAdmission.effort !== "string" || !cliAdmission.effort || typeof manager.readRunningModel !== "function" || typeof manager.getEffort !== "function" || await manager.readRunningModel(raw, raw.query) !== cliAdmission.model || await manager.getEffort(raw.sessionId) !== cliAdmission.effort))
|
|
1059
|
+
throw new Error("Actual native CLI route query/model/effort admission differs.");
|
|
1032
1060
|
await verify();
|
|
1061
|
+
if (route.dynamic.continuityTransport === "native-cli" && (raw.query !== cliAdmission.query || cliAdmission.verifyCurrent() !== void 0))
|
|
1062
|
+
throw new Error("Current native CLI route admission changed before append.");
|
|
1033
1063
|
if (manager[/* @__PURE__ */ Symbol.for("topolo.agent-pack.approvals.v1")] !== state || state.auditFailed)
|
|
1034
1064
|
throw new Error("Original native permission connection changed.");
|
|
1035
1065
|
const old = state.byId.get(route.sessionId);
|
|
@@ -1188,8 +1218,8 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
|
1188
1218
|
captureMarkdown(cwd);
|
|
1189
1219
|
const charter = resolve(cwd, "CLAUDE.md");
|
|
1190
1220
|
const transcript = realpathSync(record.transcript);
|
|
1191
|
-
context[charter] =
|
|
1192
|
-
context[transcript] =
|
|
1221
|
+
context[charter] = createHash2("sha256").update(readFileSync(charter)).digest("hex");
|
|
1222
|
+
context[transcript] = createHash2("sha256").update(readFileSync(transcript)).digest("hex");
|
|
1193
1223
|
roleContextFiles[record.desktopSessionId] = [charter, transcript];
|
|
1194
1224
|
members.push({
|
|
1195
1225
|
key: record.key,
|
|
@@ -1207,7 +1237,7 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
|
1207
1237
|
});
|
|
1208
1238
|
}
|
|
1209
1239
|
for (const file of [resolve(root, "claude/state.json"), realpathSync(policyPath)])
|
|
1210
|
-
context[file] =
|
|
1240
|
+
context[file] = createHash2("sha256").update(readFileSync(file)).digest("hex");
|
|
1211
1241
|
const activePath = resolve(root, "claude/active.json");
|
|
1212
1242
|
const active = existsSync(activePath) ? JSON.parse(readFileSync(activePath, "utf8")) : null;
|
|
1213
1243
|
const target = new Set(members.map((m) => m.key));
|
|
@@ -1224,7 +1254,7 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
|
1224
1254
|
if (!cwd.startsWith(resolve(root, "claude/workspaces", record.pack) + sep)) throw new Error("Outgoing workspace is outside the owned pack.");
|
|
1225
1255
|
captureMarkdown(cwd);
|
|
1226
1256
|
for (const file of [resolve(cwd, "CLAUDE.md"), realpathSync(record.transcript)])
|
|
1227
|
-
context[file] =
|
|
1257
|
+
context[file] = createHash2("sha256").update(readFileSync(file)).digest("hex");
|
|
1228
1258
|
outgoing.push({ key, sessionId: record.desktopSessionId, cwd, title: record.title });
|
|
1229
1259
|
}
|
|
1230
1260
|
} else if (mode === "switch") throw new Error("Configure and verify an initial active roster before switching.");
|
|
@@ -1528,7 +1558,7 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
1528
1558
|
if (!journal.startsWith(realpathSync(directory) + sep)) throw new Error("Recovery journal is outside the owned library.");
|
|
1529
1559
|
const lines = readFileSync(journal, "utf8").split("\n").filter(Boolean);
|
|
1530
1560
|
const line = lines[0];
|
|
1531
|
-
if (
|
|
1561
|
+
if (createHash2("sha256").update(line).digest("hex") !== marker.planHash) throw new Error("Recovery plan identity changed.");
|
|
1532
1562
|
const record = JSON.parse(line);
|
|
1533
1563
|
if (record.pack !== options.pack || record.operationId !== marker.operationId || !record.apply) throw new Error("Recovery pack or operation identity differs.");
|
|
1534
1564
|
plan = { ...record, approvalRoutes: record.approvalRoutes ?? [], recovery: true, noEffects: marker.format === 2 && marker.dispatchState === "not-dispatched" && !lines.map((line2) => JSON.parse(line2)).some((r) => r.kind === "attempt") };
|
|
@@ -1542,7 +1572,7 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
1542
1572
|
throw new Error("Unowned native approval connection.");
|
|
1543
1573
|
const connection = JSON.parse(readFileSync(connectionPath, "utf8"));
|
|
1544
1574
|
if (connection.pid === options.pid && connection.pack === options.pack && JSON.stringify(connection.routes) === JSON.stringify(plan.approvalRoutes) && JSON.stringify(connection.installationConsent) === JSON.stringify(plan.installationConsent)) {
|
|
1545
|
-
if (typeof connection.secretPath !== "string" || typeof connection.socket !== "string" || !resolve(connection.secretPath).startsWith(realpathSync(resolve(root, "claude/private")) + sep) || realpathSync(connection.secretPath) !== resolve(connection.secretPath) || dirname(resolve(connection.socket)) !== resolve("/tmp", "topolo-claude-pack-" +
|
|
1575
|
+
if (typeof connection.secretPath !== "string" || typeof connection.socket !== "string" || !resolve(connection.secretPath).startsWith(realpathSync(resolve(root, "claude/private")) + sep) || realpathSync(connection.secretPath) !== resolve(connection.secretPath) || dirname(resolve(connection.socket)) !== resolve("/tmp", "topolo-claude-pack-" + createHash2("sha256").update(root).digest("hex").slice(0, 16)))
|
|
1546
1576
|
throw new Error("Native approval connection is outside owned control paths.");
|
|
1547
1577
|
const secret = lstatSync(connection.secretPath);
|
|
1548
1578
|
if (!secret.isFile() || secret.isSymbolicLink() || typeof process.getuid !== "function" || secret.uid !== process.getuid() || secret.mode & 63)
|
|
@@ -1553,7 +1583,7 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
1553
1583
|
if (!plan.control) {
|
|
1554
1584
|
const directory2 = resolve(root, "claude/private");
|
|
1555
1585
|
mkdirSync(directory2, { recursive: true, mode: 448 });
|
|
1556
|
-
const socketDirectory = resolve("/tmp", "topolo-claude-pack-" +
|
|
1586
|
+
const socketDirectory = resolve("/tmp", "topolo-claude-pack-" + createHash2("sha256").update(root).digest("hex").slice(0, 16));
|
|
1557
1587
|
mkdirSync(socketDirectory, { recursive: true, mode: 448 });
|
|
1558
1588
|
for (const path of [directory2, socketDirectory]) {
|
|
1559
1589
|
const stat = lstatSync(path);
|
|
@@ -1568,7 +1598,7 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
1568
1598
|
journal = resolve(directory, operationId + ".jsonl");
|
|
1569
1599
|
const record = { kind: "plan", pack: options.pack, ...plan, apply: options.apply };
|
|
1570
1600
|
durableRecord(journal, record, "wx");
|
|
1571
|
-
marker = { format: 2, dispatchState: "not-dispatched", operationId, journal, planHash:
|
|
1601
|
+
marker = { format: 2, dispatchState: "not-dispatched", operationId, journal, planHash: createHash2("sha256").update(JSON.stringify(record)).digest("hex") };
|
|
1572
1602
|
if (options.apply) durableRecord(markerPath, marker, "wx");
|
|
1573
1603
|
syncDirectory(directory);
|
|
1574
1604
|
syncDirectory(resolve(root, "claude"));
|
|
@@ -1837,8 +1867,9 @@ function prepareClaudeDynamicLane(store, pack, policyPath, laneId) {
|
|
|
1837
1867
|
if (lstatSync(file).size > 131072) throw new Error("Dynamic lane record exceeds its schema bound.");
|
|
1838
1868
|
const record = JSON.parse(readFileSync(file, "utf8"));
|
|
1839
1869
|
const canonical = (value) => JSON.stringify(value, (_key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, item[key]])) : item);
|
|
1840
|
-
const digest = (value) =>
|
|
1841
|
-
|
|
1870
|
+
const digest = (value) => createHash2("sha256").update(canonical(value)).digest("hex");
|
|
1871
|
+
const nativeCLI = record.schemaVersion === 2 && record.plan?.schemaVersion === 2 && record.plan.continuityTransport === "native-cli";
|
|
1872
|
+
if (record.format !== "topolo-dynamic-lane" || !(record.schemaVersion === 1 || nativeCLI) || record.harness !== "claude" || record.laneId !== laneId || record.planDigest !== digest(record.plan) || record.creationReceiptDigest !== digest(record.creationReceipt) || typeof record.nativeTitle !== "string" || !record.nativeTitle || record.nativeTitle.length > 256 || record.plan.role.packId !== pack || (nativeCLI ? record.plan.nativeParentId !== null : record.plan.nativeParentId !== record.plan.intake.nativeId))
|
|
1842
1873
|
throw new Error("Durable dynamic child identity or plan digest differs.");
|
|
1843
1874
|
const stateFile = owned(resolve(root, "claude/state.json"));
|
|
1844
1875
|
const state = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
@@ -1847,8 +1878,8 @@ function prepareClaudeDynamicLane(store, pack, policyPath, laneId) {
|
|
|
1847
1878
|
if (state.harness !== "claude" || policy.format !== "topolo-agent-execution-policy" || policy.pack !== pack || policy.sourceRevision !== record.plan.role.packRevision || !policy.installationConsent)
|
|
1848
1879
|
throw new Error("Dynamic child source policy or organization-bound installation is stale.");
|
|
1849
1880
|
const context = {
|
|
1850
|
-
[stateFile]:
|
|
1851
|
-
[policyFile]:
|
|
1881
|
+
[stateFile]: createHash2("sha256").update(readFileSync(stateFile)).digest("hex"),
|
|
1882
|
+
[policyFile]: createHash2("sha256").update(readFileSync(policyFile)).digest("hex")
|
|
1852
1883
|
};
|
|
1853
1884
|
const inventories = {};
|
|
1854
1885
|
const fixed = (descriptor) => {
|
|
@@ -1861,7 +1892,7 @@ function prepareClaudeDynamicLane(store, pack, policyPath, laneId) {
|
|
|
1861
1892
|
throw new Error("Dynamic source workspace or current fixed settings differ.");
|
|
1862
1893
|
const files = [resolve(cwd, "CLAUDE.md"), resolve(cwd, "topolo-connection.json"), resolve(cwd, ".mcp.json")];
|
|
1863
1894
|
const hashes = {};
|
|
1864
|
-
for (const f of files) hashes[owned(f)] =
|
|
1895
|
+
for (const f of files) hashes[owned(f)] = createHash2("sha256").update(readFileSync(f)).digest("hex");
|
|
1865
1896
|
const connection = JSON.parse(readFileSync(files[1], "utf8"));
|
|
1866
1897
|
const expected = {
|
|
1867
1898
|
environment: descriptor.environment,
|
|
@@ -1898,6 +1929,7 @@ function prepareClaudeDynamicLane(store, pack, policyPath, laneId) {
|
|
|
1898
1929
|
planDigest: record.planDigest,
|
|
1899
1930
|
nativeId: record.nativeId,
|
|
1900
1931
|
cwd: record.creationReceipt.cwd,
|
|
1932
|
+
...nativeCLI ? { continuityTransport: "native-cli", continuity: record.plan.continuity } : {},
|
|
1901
1933
|
title: record.nativeTitle,
|
|
1902
1934
|
source,
|
|
1903
1935
|
intake,
|
|
@@ -1981,7 +2013,7 @@ function prepareClaudePrivateHelperConnection(store, pack, policyPath, sourceRol
|
|
|
1981
2013
|
const policyBytes = readFileSync(policyFile), policy = JSON.parse(policyBytes.toString("utf8")), consent = policy.installationConsent;
|
|
1982
2014
|
if (!consent || !["development", "staging", "production"].includes(consent.environment) || typeof consent.contextKey !== "string")
|
|
1983
2015
|
throw new Error("Existing approved per-environment private connection required.");
|
|
1984
|
-
const bindingFile = resolve(root, "connections", pack, consent.environment,
|
|
2016
|
+
const bindingFile = resolve(root, "connections", pack, consent.environment, createHash2("sha256").update(consent.contextKey).digest("hex") + ".json");
|
|
1985
2017
|
const bindingBytes = readFileSync(bindingFile), binding = JSON.parse(bindingBytes.toString("utf8"));
|
|
1986
2018
|
const connection = {
|
|
1987
2019
|
format: "topolo-private-helper-connection",
|
|
@@ -1991,9 +2023,9 @@ function prepareClaudePrivateHelperConnection(store, pack, policyPath, sourceRol
|
|
|
1991
2023
|
sourceRoleKey,
|
|
1992
2024
|
policyFile,
|
|
1993
2025
|
bindingFile,
|
|
1994
|
-
policyDigest:
|
|
1995
|
-
bindingDigest:
|
|
1996
|
-
stateDigest:
|
|
2026
|
+
policyDigest: createHash2("sha256").update(policyBytes).digest("hex"),
|
|
2027
|
+
bindingDigest: createHash2("sha256").update(bindingBytes).digest("hex"),
|
|
2028
|
+
stateDigest: createHash2("sha256").update(readFileSync(resolve(root, "claude/state.json"))).digest("hex"),
|
|
1997
2029
|
profile: binding.privateCredentialProfile
|
|
1998
2030
|
};
|
|
1999
2031
|
privateClaudeHelperEnvironment(connection, filesystem, cryptography, paths, process.env);
|
|
@@ -2025,6 +2057,11 @@ function runClaudeLaneHelper(root, pack, policyPath, action, payload, source, co
|
|
|
2025
2057
|
return Promise.reject(new Error("Closed typed native lifecycle helper required."));
|
|
2026
2058
|
const encoded = JSON.stringify(payload);
|
|
2027
2059
|
if (Buffer.byteLength(encoded) > 2097152) return Promise.reject(new Error("Own-child native transcript exceeds bounded transport; no broad history read."));
|
|
2060
|
+
try {
|
|
2061
|
+
requireClaudePrivateQueryEffectQualified("helper", payload);
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
return Promise.reject(error);
|
|
2064
|
+
}
|
|
2028
2065
|
const selectedRole = payload.sourceRoleKey ?? payload.request?.sourceRoleKey ?? payload.plan?.role?.key ?? payload.operation?.record?.plan?.role?.key;
|
|
2029
2066
|
if (!connection || root !== connection.root || pack !== connection.pack || policyPath !== connection.policyFile || selectedRole !== void 0 && selectedRole !== connection.sourceRoleKey)
|
|
2030
2067
|
return Promise.reject(new Error("Private helper arguments differ from the exact bound source connection."));
|
|
@@ -2072,7 +2109,14 @@ function runClaudeLaneHelper(root, pack, policyPath, action, payload, source, co
|
|
|
2072
2109
|
child.stdin.end(encoded);
|
|
2073
2110
|
});
|
|
2074
2111
|
}
|
|
2075
|
-
function requireClaudePrivateQueryEffectQualified(operation) {
|
|
2112
|
+
function requireClaudePrivateQueryEffectQualified(operation, input) {
|
|
2113
|
+
const candidates = [input, input?.request, input?.plan, input?.record?.plan, input?.operation?.record?.plan];
|
|
2114
|
+
const transports = candidates.map((value) => value?.continuityTransport).filter((value) => value !== void 0);
|
|
2115
|
+
if (transports.some((value) => !["native-cli", "private-mcp"].includes(value)) || new Set(transports).size > 1)
|
|
2116
|
+
throw new Error("Closed explicit continuity transport required; no fallback.");
|
|
2117
|
+
const transport = transports[0];
|
|
2118
|
+
if (transport === "native-cli" || candidates.some((value) => value?.schemaVersion === 2))
|
|
2119
|
+
throw Object.assign(new Error("native_cli_startup_unqualified: current startup, profile refresh authority and native result producers are unavailable."), { code: "native_cli_startup_unqualified" });
|
|
2076
2120
|
if (["create", "configure", "context-start", "fixture-start", "retire", "approval-once"].includes(operation))
|
|
2077
2121
|
throw Object.assign(
|
|
2078
2122
|
new Error("native_private_query_prestart_contract_unqualified: private native query startup, exclusion and owned lifecycle are not qualified."),
|
|
@@ -2082,12 +2126,12 @@ function requireClaudePrivateQueryEffectQualified(operation) {
|
|
|
2082
2126
|
function claudeLaneExpression(options, source, journal, prepared) {
|
|
2083
2127
|
if (!Number.isSafeInteger(options.pid) || options.pid < 1 || !["plan", "create", "create-read", "adopt", "read", "configure", "context-start", "context-read", "fixture-start", "fixture-read", "retire"].includes(options.operation))
|
|
2084
2128
|
throw new Error("Closed typed Claude lane operation required.");
|
|
2085
|
-
requireClaudePrivateQueryEffectQualified(options.operation);
|
|
2129
|
+
requireClaudePrivateQueryEffectQualified(options.operation, options.input);
|
|
2086
2130
|
if (!options.privateConnection) throw new Error("Explicit approved private helper connection required before native access.");
|
|
2087
2131
|
return `(async()=>{
|
|
2088
2132
|
const expected=${JSON.stringify({ ...options, qualifier: CLAUDE_NATIVE_QUALIFIER, source, journal })};
|
|
2089
2133
|
const requireClaudePrivateQueryEffectQualified=(${requireClaudePrivateQueryEffectQualified.toString()});
|
|
2090
|
-
requireClaudePrivateQueryEffectQualified(expected.operation);
|
|
2134
|
+
requireClaudePrivateQueryEffectQualified(expected.operation,expected.input);
|
|
2091
2135
|
if(process.pid!==expected.pid || typeof process.getBuiltinModule!=='function')throw new Error('Native process identity changed.');
|
|
2092
2136
|
const load=process.getBuiltinModule('module').createRequire(process.resourcesPath+'/app.asar/package.json');
|
|
2093
2137
|
const fs=load('node:fs'),crypto=load('node:crypto'),path=load('node:path');
|
|
@@ -2203,7 +2247,7 @@ function claudeLaneExpression(options, source, journal, prepared) {
|
|
|
2203
2247
|
if(!secretInfo.isFile()||secretInfo.isSymbolicLink()||secretInfo.uid!==process.getuid()||secretInfo.mode&63)throw new Error('Unowned private native control secret.');
|
|
2204
2248
|
const secret=JSON.parse(fs.readFileSync(control.secretPath,'utf8'));
|
|
2205
2249
|
const ownedOperation=async op=>{
|
|
2206
|
-
requireClaudePrivateQueryEffectQualified(op.kind==='approval-respond'&&op.decision==='once'?'approval-once':op.kind);
|
|
2250
|
+
requireClaudePrivateQueryEffectQualified(op.kind==='approval-respond'&&op.decision==='once'?'approval-once':op.kind,{continuityTransport:nativePlan.continuityTransport});
|
|
2207
2251
|
qualify();const current=await helper('read',{laneId:nativePlan.laneId});
|
|
2208
2252
|
const scopedPlan={...nativePlan,lifecycleState:current.state,deadline:Date.now()+200000};
|
|
2209
2253
|
if(op.kind.startsWith('approval-')){
|
|
@@ -2270,7 +2314,7 @@ function claudeLaneExpression(options, source, journal, prepared) {
|
|
|
2270
2314
|
})()`;
|
|
2271
2315
|
}
|
|
2272
2316
|
async function operateClaudeLane(options, evaluate = evaluateClaudeMain) {
|
|
2273
|
-
requireClaudePrivateQueryEffectQualified(options.operation);
|
|
2317
|
+
requireClaudePrivateQueryEffectQualified(options.operation, options.input);
|
|
2274
2318
|
validateInspectorEndpoint(options.endpoint);
|
|
2275
2319
|
const root = realpathSync(options.store);
|
|
2276
2320
|
const policy = realpathSync(options.policy);
|
|
@@ -2285,6 +2329,7 @@ async function operateClaudeLane(options, evaluate = evaluateClaudeMain) {
|
|
|
2285
2329
|
const journal = resolve(directory, randomUUID() + ".jsonl");
|
|
2286
2330
|
durableRecord(journal, { operation: options.operation, pid: options.pid, at: Date.now() }, "wx");
|
|
2287
2331
|
const prepared = ["plan", "create", "create-read", "adopt"].includes(options.operation) ? void 0 : prepareClaudeDynamicLane(root, options.pack, policy, options.input.laneId);
|
|
2332
|
+
if (prepared?.record.plan.continuityTransport === "native-cli") requireClaudePrivateQueryEffectQualified(options.operation, prepared.record.plan);
|
|
2288
2333
|
let sourceRoleKey = prepared?.record.plan.role.key ?? (options.operation === "plan" ? options.input.request?.sourceRoleKey : options.input.plan?.role?.key);
|
|
2289
2334
|
if (["create", "create-read"].includes(options.operation)) {
|
|
2290
2335
|
if (typeof options.input.laneId !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(options.input.laneId)) throw new Error("Exact existing creation lane required.");
|
|
@@ -2307,8 +2352,8 @@ async function operateClaudeLane(options, evaluate = evaluateClaudeMain) {
|
|
|
2307
2352
|
mkdirSync(directory2, { mode: 448 });
|
|
2308
2353
|
const secretPath = resolve(directory2, "secret.json");
|
|
2309
2354
|
durableRecord(secretPath, { token: randomBytes(32).toString("hex") }, "wx");
|
|
2310
|
-
const identity =
|
|
2311
|
-
const socketDirectory = resolve("/tmp", "topolo-claude-" + process.getuid?.() + "-" +
|
|
2355
|
+
const identity = createHash2("sha256").update(JSON.stringify({ pid: options.pid, laneId, planDigest: prepared.record.planDigest, qualifier: CLAUDE_NATIVE_QUALIFIER })).digest("hex");
|
|
2356
|
+
const socketDirectory = resolve("/tmp", "topolo-claude-" + process.getuid?.() + "-" + createHash2("sha256").update(root + "\0" + laneId).digest("hex").slice(0, 24));
|
|
2312
2357
|
mkdirSync(socketDirectory, { mode: 448 });
|
|
2313
2358
|
const socket = resolve(socketDirectory, "control.sock");
|
|
2314
2359
|
control = { socket, secretPath, identity };
|
|
@@ -2335,7 +2380,7 @@ async function operateClaudeLane(options, evaluate = evaluateClaudeMain) {
|
|
|
2335
2380
|
}
|
|
2336
2381
|
}
|
|
2337
2382
|
function operateClaudeLaneControl(options, request = requestClaudeApprovalControl) {
|
|
2338
|
-
requireClaudePrivateQueryEffectQualified(options.operation.kind === "approval-respond" && options.operation.decision === "once" ? "approval-once" : options.operation.kind);
|
|
2383
|
+
requireClaudePrivateQueryEffectQualified(options.operation.kind === "approval-respond" && options.operation.decision === "once" ? "approval-once" : options.operation.kind, options.operation);
|
|
2339
2384
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.connection)) throw new Error("Exact owned native lane connection ID required.");
|
|
2340
2385
|
const root = realpathSync(options.store);
|
|
2341
2386
|
const directory = resolve(root, "claude/dynamic-controls", options.connection);
|