@topolo/cli 0.12.33 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/agent-packs/README.md +495 -0
- package/agent-packs/pyproject.toml +16 -0
- package/agent-packs/requirements-test.txt +1 -0
- package/agent-packs/src/topolo_agent_pack/__init__.py +1 -0
- package/agent-packs/src/topolo_agent_pack/blueprint.py +57 -0
- package/agent-packs/src/topolo_agent_pack/claude.py +265 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +443 -0
- package/agent-packs/src/topolo_agent_pack/execution.py +154 -0
- package/agent-packs/src/topolo_agent_pack/fresh.py +158 -0
- package/agent-packs/src/topolo_agent_pack/live.py +403 -0
- package/agent-packs/src/topolo_agent_pack/native.py +645 -0
- package/agent-packs/src/topolo_agent_pack/policy.py +92 -0
- package/agent-packs/src/topolo_agent_pack/storage.py +271 -0
- package/agent-packs/src/topolo_agent_pack/topolo.py +321 -0
- package/agent-packs/templates/career/AGENTS.md +17 -0
- package/agent-packs/templates/career/README.md +14 -0
- package/agent-packs/templates/career/agents/intakes/career-intake/AGENTS.md +21 -0
- package/agent-packs/templates/career/agents/specialists/application-preparation/AGENTS.md +21 -0
- package/agent-packs/templates/career/agents/specialists/application-tracker/AGENTS.md +21 -0
- package/agent-packs/templates/career/agents/specialists/interview-coach/AGENTS.md +21 -0
- package/agent-packs/templates/career/agents/specialists/opportunity-research/AGENTS.md +21 -0
- package/agent-packs/templates/career/agents/specialists/profile-cv/AGENTS.md +21 -0
- package/agent-packs/templates/career/pack.json +124 -0
- package/agent-packs/templates/career/shared/PACK.md +22 -0
- package/agent-packs/templates/career/shared/TOPOLO.md +13 -0
- package/agent-packs/templates/career/shared/WORKFLOW.md +13 -0
- package/agent-packs/templates/career/skills/topolo/SKILL.md +305 -0
- package/agent-packs/templates/clients/AGENTS.md +17 -0
- package/agent-packs/templates/clients/README.md +14 -0
- package/agent-packs/templates/clients/agents/intakes/client-intake/AGENTS.md +21 -0
- package/agent-packs/templates/clients/agents/specialists/acceptance-review/AGENTS.md +21 -0
- package/agent-packs/templates/clients/agents/specialists/delivery-planner/AGENTS.md +21 -0
- package/agent-packs/templates/clients/agents/specialists/discovery/AGENTS.md +21 -0
- package/agent-packs/templates/clients/agents/specialists/proposal-writer/AGENTS.md +21 -0
- package/agent-packs/templates/clients/agents/specialists/solution-design/AGENTS.md +21 -0
- package/agent-packs/templates/clients/pack.json +126 -0
- package/agent-packs/templates/clients/shared/PACK.md +22 -0
- package/agent-packs/templates/clients/shared/TOPOLO.md +13 -0
- package/agent-packs/templates/clients/shared/WORKFLOW.md +13 -0
- package/agent-packs/templates/clients/skills/topolo/SKILL.md +305 -0
- package/agent-packs/templates/creative/AGENTS.md +17 -0
- package/agent-packs/templates/creative/README.md +14 -0
- package/agent-packs/templates/creative/agents/intakes/creative-director/AGENTS.md +21 -0
- package/agent-packs/templates/creative/agents/specialists/brand-copy/AGENTS.md +21 -0
- package/agent-packs/templates/creative/agents/specialists/media-production/AGENTS.md +21 -0
- package/agent-packs/templates/creative/agents/specialists/quality-review/AGENTS.md +21 -0
- package/agent-packs/templates/creative/agents/specialists/storytelling/AGENTS.md +21 -0
- package/agent-packs/templates/creative/agents/specialists/visual-design/AGENTS.md +21 -0
- package/agent-packs/templates/creative/pack.json +128 -0
- package/agent-packs/templates/creative/shared/PACK.md +22 -0
- package/agent-packs/templates/creative/shared/TOPOLO.md +13 -0
- package/agent-packs/templates/creative/shared/WORKFLOW.md +13 -0
- package/agent-packs/templates/creative/skills/topolo/SKILL.md +305 -0
- package/agent-packs/templates/founder/AGENTS.md +17 -0
- package/agent-packs/templates/founder/README.md +14 -0
- package/agent-packs/templates/founder/agents/intakes/founder-intake/AGENTS.md +21 -0
- package/agent-packs/templates/founder/agents/specialists/commercial-modelling/AGENTS.md +21 -0
- package/agent-packs/templates/founder/agents/specialists/customer-research/AGENTS.md +21 -0
- package/agent-packs/templates/founder/agents/specialists/decision-review/AGENTS.md +21 -0
- package/agent-packs/templates/founder/agents/specialists/partnerships/AGENTS.md +21 -0
- package/agent-packs/templates/founder/agents/specialists/strategy/AGENTS.md +21 -0
- package/agent-packs/templates/founder/pack.json +126 -0
- package/agent-packs/templates/founder/shared/PACK.md +22 -0
- package/agent-packs/templates/founder/shared/TOPOLO.md +13 -0
- package/agent-packs/templates/founder/shared/WORKFLOW.md +13 -0
- package/agent-packs/templates/founder/skills/topolo/SKILL.md +305 -0
- package/agent-packs/templates/learning/AGENTS.md +17 -0
- package/agent-packs/templates/learning/README.md +14 -0
- package/agent-packs/templates/learning/agents/intakes/learning-intake/AGENTS.md +21 -0
- package/agent-packs/templates/learning/agents/specialists/critical-reviewer/AGENTS.md +21 -0
- package/agent-packs/templates/learning/agents/specialists/curriculum-designer/AGENTS.md +21 -0
- package/agent-packs/templates/learning/agents/specialists/knowledge-steward/AGENTS.md +21 -0
- package/agent-packs/templates/learning/agents/specialists/practice-coach/AGENTS.md +21 -0
- package/agent-packs/templates/learning/agents/specialists/tutor/AGENTS.md +21 -0
- package/agent-packs/templates/learning/pack.json +124 -0
- package/agent-packs/templates/learning/shared/PACK.md +22 -0
- package/agent-packs/templates/learning/shared/TOPOLO.md +13 -0
- package/agent-packs/templates/learning/shared/WORKFLOW.md +13 -0
- package/agent-packs/templates/learning/skills/topolo/SKILL.md +305 -0
- package/agent-packs/templates/personal/AGENTS.md +17 -0
- package/agent-packs/templates/personal/README.md +14 -0
- package/agent-packs/templates/personal/agents/intakes/personal-intake/AGENTS.md +21 -0
- package/agent-packs/templates/personal/agents/specialists/correspondence/AGENTS.md +21 -0
- package/agent-packs/templates/personal/agents/specialists/document-assistant/AGENTS.md +21 -0
- package/agent-packs/templates/personal/agents/specialists/planning/AGENTS.md +21 -0
- package/agent-packs/templates/personal/agents/specialists/research/AGENTS.md +21 -0
- package/agent-packs/templates/personal/agents/specialists/travel-logistics/AGENTS.md +21 -0
- package/agent-packs/templates/personal/pack.json +125 -0
- package/agent-packs/templates/personal/shared/PACK.md +22 -0
- package/agent-packs/templates/personal/shared/TOPOLO.md +13 -0
- package/agent-packs/templates/personal/shared/WORKFLOW.md +13 -0
- package/agent-packs/templates/personal/skills/topolo/SKILL.md +305 -0
- package/agent-packs/templates/research/AGENTS.md +17 -0
- package/agent-packs/templates/research/README.md +14 -0
- package/agent-packs/templates/research/agents/intakes/research-intake/AGENTS.md +21 -0
- package/agent-packs/templates/research/agents/specialists/adversarial-review/AGENTS.md +21 -0
- package/agent-packs/templates/research/agents/specialists/data-analysis/AGENTS.md +21 -0
- package/agent-packs/templates/research/agents/specialists/source-research/AGENTS.md +21 -0
- package/agent-packs/templates/research/agents/specialists/synthesis/AGENTS.md +21 -0
- package/agent-packs/templates/research/agents/specialists/technical-research/AGENTS.md +21 -0
- package/agent-packs/templates/research/pack.json +124 -0
- package/agent-packs/templates/research/shared/PACK.md +22 -0
- package/agent-packs/templates/research/shared/TOPOLO.md +13 -0
- package/agent-packs/templates/research/shared/WORKFLOW.md +13 -0
- package/agent-packs/templates/research/skills/topolo/SKILL.md +305 -0
- package/agent-packs/templates/ventures/AGENTS.md +17 -0
- package/agent-packs/templates/ventures/README.md +14 -0
- package/agent-packs/templates/ventures/agents/intakes/venture-intake/AGENTS.md +21 -0
- package/agent-packs/templates/ventures/agents/specialists/customer-discovery/AGENTS.md +21 -0
- package/agent-packs/templates/ventures/agents/specialists/economics/AGENTS.md +21 -0
- package/agent-packs/templates/ventures/agents/specialists/experiment-design/AGENTS.md +21 -0
- package/agent-packs/templates/ventures/agents/specialists/market-research/AGENTS.md +21 -0
- package/agent-packs/templates/ventures/agents/specialists/product-concept/AGENTS.md +21 -0
- package/agent-packs/templates/ventures/pack.json +126 -0
- package/agent-packs/templates/ventures/shared/PACK.md +22 -0
- package/agent-packs/templates/ventures/shared/TOPOLO.md +13 -0
- package/agent-packs/templates/ventures/shared/WORKFLOW.md +13 -0
- package/agent-packs/templates/ventures/skills/topolo/SKILL.md +305 -0
- package/agent-packs/topolo-agent-pack +8 -0
- package/dist/claude-pack-backend-IDQQX3DG.js +555 -0
- package/dist/index.js +143 -58
- package/package.json +7 -4
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Portable per-role execution intent, distinct from harness-applied settings.
|
|
2
|
+
|
|
3
|
+
Rendering a policy never changes a session, permission grant, or pack revision.
|
|
4
|
+
Adapters must independently apply and read back every required setting.
|
|
5
|
+
"""
|
|
6
|
+
import copy
|
|
7
|
+
|
|
8
|
+
from .storage import PackError, component_key
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
GROUPS = {'specialists': 'Topolo Specialists', 'operators': 'Topolo Operators',
|
|
12
|
+
'operator-skills': 'Operator Skills'}
|
|
13
|
+
DEEP_ROLES = {'product-direction-01', 'product-direction-02', 'system-architecture-01',
|
|
14
|
+
'system-architecture-02', 'architecture', 'security-permissions-review',
|
|
15
|
+
'adversarial-qa', 'incident-triage', 'release-recovery', 'execution-readiness',
|
|
16
|
+
'privacy-and-agreement-review', 'workflow-integration-specialist',
|
|
17
|
+
'financial-evidence-specialist', 'accounting-evidence-review',
|
|
18
|
+
'access-and-privacy'}
|
|
19
|
+
BUILD_ROLES = {'build-01', 'build-02', 'build-03', 'developer-environment-steward',
|
|
20
|
+
'platform-package-steward', 'worktree-retirement'}
|
|
21
|
+
PROFILES = {
|
|
22
|
+
'intake': {'tier': 'reasoning', 'effort': 'medium', 'localAccess': 'read-only'},
|
|
23
|
+
'analysis': {'tier': 'reasoning', 'effort': 'high', 'localAccess': 'read-only'},
|
|
24
|
+
'review': {'tier': 'workhorse', 'effort': 'high', 'localAccess': 'read-only'},
|
|
25
|
+
'build': {'tier': 'workhorse', 'effort': 'high', 'localAccess': 'workspace-write'},
|
|
26
|
+
'operator': {'tier': 'workhorse', 'effort': 'medium', 'localAccess': 'workspace-write'},
|
|
27
|
+
}
|
|
28
|
+
MODELS = {'codex': {'reasoning': 'gpt-6-astra', 'workhorse': 'gpt-6.1-sol'},
|
|
29
|
+
'claude': {'reasoning': 'claude-opus-5-5', 'workhorse': 'claude-sonnet-5-5'}}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def role_profile(agent):
|
|
33
|
+
explicit = agent.get('executionProfile')
|
|
34
|
+
if explicit is not None:
|
|
35
|
+
if explicit not in PROFILES:
|
|
36
|
+
raise PackError('Unknown execution profile for ' + component_key(agent))
|
|
37
|
+
return explicit
|
|
38
|
+
if agent['set'] == 'intakes':
|
|
39
|
+
return 'intake'
|
|
40
|
+
if agent['id'] in DEEP_ROLES:
|
|
41
|
+
return 'analysis'
|
|
42
|
+
if agent['id'] in BUILD_ROLES:
|
|
43
|
+
return 'build'
|
|
44
|
+
if agent['set'] == 'specialists':
|
|
45
|
+
return 'review'
|
|
46
|
+
return 'operator'
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def settings(agent, harness):
|
|
50
|
+
if harness not in MODELS:
|
|
51
|
+
raise PackError('Execution settings adapter is not qualified: ' + harness)
|
|
52
|
+
profile = role_profile(agent)
|
|
53
|
+
spec = PROFILES[profile]
|
|
54
|
+
model = MODELS[harness][spec['tier']]
|
|
55
|
+
if harness == 'codex':
|
|
56
|
+
native = {'model': model, 'model_reasoning_effort': spec['effort'],
|
|
57
|
+
'approval_policy': 'on-request', 'approvals_reviewer': 'user',
|
|
58
|
+
'sandbox_mode': spec['localAccess'],
|
|
59
|
+
'sandbox_workspace_write': {'network_access': False, 'writable_roots': [],
|
|
60
|
+
'exclude_slash_tmp': True, 'exclude_tmpdir_env_var': True},
|
|
61
|
+
'apps': {'_default': {'default_tools_approval_mode': 'writes'}}}
|
|
62
|
+
else:
|
|
63
|
+
native = {'model': model, 'effortLevel': spec['effort'],
|
|
64
|
+
'permissions': {'defaultMode': 'acceptEdits' if profile == 'build' else 'default'}}
|
|
65
|
+
return {'profile': profile, 'nativeSettings': native,
|
|
66
|
+
'localAccessIntent': spec['localAccess'], 'applied': False,
|
|
67
|
+
# Claude Manual is an approval mode, not a read-only filesystem sandbox.
|
|
68
|
+
'requiresAdditionalLocalConfinement': harness == 'claude' and spec['localAccess'] == 'read-only'}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def render(manifest, revision, agents, harnesses=('codex', 'claude')):
|
|
72
|
+
rows = []
|
|
73
|
+
for agent in agents:
|
|
74
|
+
rows.append({'key': component_key(agent), 'title': agent['title'],
|
|
75
|
+
'sidebar': {'group': GROUPS.get(agent['set'], agent['set']),
|
|
76
|
+
'pinned': agent.get('pinned', False)},
|
|
77
|
+
'harnesses': {h: settings(agent, h) for h in harnesses}})
|
|
78
|
+
return {'format': 'topolo-agent-execution-policy', 'schemaVersion': 1,
|
|
79
|
+
'pack': manifest['packId'], 'sourceRevision': revision, 'agents': rows,
|
|
80
|
+
'applied': False,
|
|
81
|
+
'contextPolicy': {'load': ['role charter', 'current task', 'relevant retrieved memory'],
|
|
82
|
+
'sharedMemory': 'retrieve by task and user or organization scope',
|
|
83
|
+
'history': 'preserve, do not copy between roles',
|
|
84
|
+
'skills': 'load on demand', 'toolResults': 'bounded relevant excerpts',
|
|
85
|
+
'idleModelTurns': False, 'backgroundPolling': False,
|
|
86
|
+
'handoffs': 'one concise outcome with evidence references'},
|
|
87
|
+
'verification': {'required': ['session identity', 'sidebar membership', 'pins',
|
|
88
|
+
'model', 'effort', 'permissions', 'context identity'],
|
|
89
|
+
'unsupportedSettings': 'refuse activation; never silently substitute'},
|
|
90
|
+
'authority': {'externalActions': 'existing human authorization and platform access controls',
|
|
91
|
+
'packMembershipGrantsAuthority': False},
|
|
92
|
+
'profiles': copy.deepcopy(PROFILES)}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Immutable portable library, checked object storage, and durable transactions."""
|
|
2
|
+
import contextlib
|
|
3
|
+
import fcntl
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import pathlib
|
|
8
|
+
import re
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
import zipfile
|
|
13
|
+
|
|
14
|
+
class PackError(Exception):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def safe_id(value):
|
|
18
|
+
if not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}", value) or value in {".", ".."}:
|
|
19
|
+
raise PackError(f"Invalid identifier: {value!r}")
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
def slug(value):
|
|
23
|
+
result = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
|
|
24
|
+
return result[:110] or "agent"
|
|
25
|
+
|
|
26
|
+
def canonical(value):
|
|
27
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
|
|
28
|
+
|
|
29
|
+
def now():
|
|
30
|
+
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
31
|
+
|
|
32
|
+
def read_json(path, default=None):
|
|
33
|
+
path = pathlib.Path(path)
|
|
34
|
+
if not path.exists() and default is not None:
|
|
35
|
+
return default
|
|
36
|
+
return json.loads(path.read_text())
|
|
37
|
+
|
|
38
|
+
def sync_directory(path):
|
|
39
|
+
fd=os.open(path,os.O_RDONLY)
|
|
40
|
+
try: os.fsync(fd)
|
|
41
|
+
finally: os.close(fd)
|
|
42
|
+
|
|
43
|
+
def sync_tree(root):
|
|
44
|
+
"""Flush staged native files before publishing the durable active pointer."""
|
|
45
|
+
for folder,dirs,files in os.walk(root,followlinks=False):
|
|
46
|
+
for name in files:
|
|
47
|
+
path=pathlib.Path(folder)/name
|
|
48
|
+
if path.is_symlink(): continue
|
|
49
|
+
with path.open('rb') as stream: os.fsync(stream.fileno())
|
|
50
|
+
sync_directory(folder)
|
|
51
|
+
|
|
52
|
+
def atomic_json(path, value):
|
|
53
|
+
path = pathlib.Path(path)
|
|
54
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
55
|
+
fd, tmp = tempfile.mkstemp(prefix=".pending-", dir=path.parent)
|
|
56
|
+
try:
|
|
57
|
+
with os.fdopen(fd, "wb") as stream:
|
|
58
|
+
stream.write(canonical(value) + b"\n")
|
|
59
|
+
stream.flush()
|
|
60
|
+
os.fsync(stream.fileno())
|
|
61
|
+
os.chmod(tmp, 0o600)
|
|
62
|
+
os.replace(tmp, path)
|
|
63
|
+
dirfd = os.open(path.parent, os.O_RDONLY)
|
|
64
|
+
try:
|
|
65
|
+
os.fsync(dirfd)
|
|
66
|
+
finally:
|
|
67
|
+
os.close(dirfd)
|
|
68
|
+
finally:
|
|
69
|
+
if os.path.exists(tmp):
|
|
70
|
+
os.unlink(tmp)
|
|
71
|
+
|
|
72
|
+
class Library:
|
|
73
|
+
def __init__(self, root):
|
|
74
|
+
self.root = pathlib.Path(root).expanduser().resolve()
|
|
75
|
+
self.root.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
76
|
+
for name in ["objects", "packs", "runtimes", "journals"]:
|
|
77
|
+
(self.root / name).mkdir(exist_ok=True, mode=0o700)
|
|
78
|
+
|
|
79
|
+
@contextlib.contextmanager
|
|
80
|
+
def lock(self):
|
|
81
|
+
with (self.root / ".lock").open("a+") as stream:
|
|
82
|
+
os.chmod(stream.name, 0o600)
|
|
83
|
+
try:
|
|
84
|
+
fcntl.flock(stream, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
85
|
+
except BlockingIOError:
|
|
86
|
+
raise PackError("Another topolo-agent-pack operation owns this library. Retry after it finishes.")
|
|
87
|
+
yield
|
|
88
|
+
|
|
89
|
+
def put(self, source, limit=None):
|
|
90
|
+
if limit is not None and (not isinstance(limit,int) or limit<0): raise PackError('Invalid capture byte boundary')
|
|
91
|
+
digest = hashlib.sha256()
|
|
92
|
+
fd, temp = tempfile.mkstemp(dir=self.root / "objects", prefix=".capture-")
|
|
93
|
+
size = 0
|
|
94
|
+
try:
|
|
95
|
+
with os.fdopen(fd, "wb") as dest:
|
|
96
|
+
if isinstance(source, bytes):
|
|
97
|
+
digest.update(source); dest.write(source); size = len(source)
|
|
98
|
+
else:
|
|
99
|
+
source = pathlib.Path(source)
|
|
100
|
+
before = source.stat()
|
|
101
|
+
with source.open("rb") as stream:
|
|
102
|
+
while chunk := stream.read(min(1024*1024,limit-size) if limit is not None else 1024*1024):
|
|
103
|
+
digest.update(chunk); dest.write(chunk); size += len(chunk)
|
|
104
|
+
after = source.stat()
|
|
105
|
+
changed=(before.st_size,before.st_mtime_ns,before.st_ino)!=(after.st_size,after.st_mtime_ns,after.st_ino)
|
|
106
|
+
if limit is not None:
|
|
107
|
+
changed=before.st_ino!=after.st_ino or size!=limit or after.st_size<limit
|
|
108
|
+
if changed:
|
|
109
|
+
raise PackError(f"File changed during capture: {source}")
|
|
110
|
+
dest.flush(); os.fsync(dest.fileno())
|
|
111
|
+
key = digest.hexdigest()
|
|
112
|
+
target = self.root / "objects" / key
|
|
113
|
+
if not target.exists():
|
|
114
|
+
os.chmod(temp, 0o600); os.replace(temp, target)
|
|
115
|
+
sync_directory(target.parent)
|
|
116
|
+
else: self.object({'sha256':key,'bytes':size})
|
|
117
|
+
return {"sha256": key, "bytes": size}
|
|
118
|
+
finally:
|
|
119
|
+
if os.path.exists(temp):
|
|
120
|
+
os.unlink(temp)
|
|
121
|
+
|
|
122
|
+
def object(self, ref, verify=True):
|
|
123
|
+
key = ref.get("sha256", "")
|
|
124
|
+
if not re.fullmatch(r"[a-f0-9]{64}", key):
|
|
125
|
+
raise PackError("Invalid object digest in pack")
|
|
126
|
+
path = self.root / "objects" / key
|
|
127
|
+
if not path.is_file() or path.is_symlink():
|
|
128
|
+
raise PackError(f"Missing object: {key}")
|
|
129
|
+
if verify:
|
|
130
|
+
h = hashlib.sha256()
|
|
131
|
+
with path.open("rb") as f:
|
|
132
|
+
while chunk := f.read(1024 * 1024): h.update(chunk)
|
|
133
|
+
if h.hexdigest() != key or path.stat().st_size != ref["bytes"]:
|
|
134
|
+
raise PackError(f"Corrupt object: {key}")
|
|
135
|
+
return path
|
|
136
|
+
|
|
137
|
+
def save(self, manifest):
|
|
138
|
+
validate_manifest(manifest)
|
|
139
|
+
revision = hashlib.sha256(canonical(manifest)).hexdigest()
|
|
140
|
+
pack = safe_id(manifest["packId"])
|
|
141
|
+
path = self.root / "packs" / pack / "revisions" / (revision + ".json")
|
|
142
|
+
if not path.exists(): atomic_json(path, manifest)
|
|
143
|
+
atomic_json(self.root / "packs" / pack / "head.json", {"revision": revision})
|
|
144
|
+
return revision
|
|
145
|
+
|
|
146
|
+
def load(self, name, revision=None):
|
|
147
|
+
name = safe_id(name)
|
|
148
|
+
if revision is None:
|
|
149
|
+
head = self.root / "packs" / name / "head.json"
|
|
150
|
+
if not head.exists(): raise PackError(f"Pack {name!r} is not installed. Capture or install it first.")
|
|
151
|
+
revision = read_json(head)["revision"]
|
|
152
|
+
if not re.fullmatch(r"[a-f0-9]{64}", revision): raise PackError("Invalid revision")
|
|
153
|
+
path = self.root / "packs" / name / "revisions" / (revision + ".json")
|
|
154
|
+
if not path.exists(): raise PackError(f"Missing revision {name}@{revision}")
|
|
155
|
+
value = read_json(path)
|
|
156
|
+
validate_manifest(value)
|
|
157
|
+
if hashlib.sha256(canonical(value)).hexdigest() != revision: raise PackError("Manifest integrity mismatch")
|
|
158
|
+
return value, revision
|
|
159
|
+
|
|
160
|
+
def active(self):
|
|
161
|
+
return read_json(self.root / "active.json", {})
|
|
162
|
+
|
|
163
|
+
def pending(self):
|
|
164
|
+
return [p for p in (self.root / "journals").glob("*.json") if read_json(p).get("phase") not in {"complete", "rolled-back"}]
|
|
165
|
+
|
|
166
|
+
def export(self, name, path):
|
|
167
|
+
manifest, revision = self.load(name)
|
|
168
|
+
target = pathlib.Path(path).expanduser().resolve()
|
|
169
|
+
if target.exists(): raise PackError(f"Export destination already exists: {target}")
|
|
170
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
refs = references(manifest)
|
|
172
|
+
fd, temp = tempfile.mkstemp(dir=target.parent, prefix=".pack-export-"); os.close(fd)
|
|
173
|
+
try:
|
|
174
|
+
with zipfile.ZipFile(temp, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=4) as archive:
|
|
175
|
+
archive.writestr("manifest.json", canonical(manifest))
|
|
176
|
+
for key, ref in sorted(refs.items()): archive.write(self.object(ref), "objects/" + key)
|
|
177
|
+
os.chmod(temp, 0o600)
|
|
178
|
+
with open(temp,'rb') as stream: os.fsync(stream.fileno())
|
|
179
|
+
os.link(temp,target);os.unlink(temp);sync_directory(target.parent)
|
|
180
|
+
finally:
|
|
181
|
+
if os.path.exists(temp): os.unlink(temp)
|
|
182
|
+
return {"pack": name, "revision": revision, "bundle": str(target), "objects": len(refs)}
|
|
183
|
+
|
|
184
|
+
def install(self, path, publish=True):
|
|
185
|
+
path = pathlib.Path(path).expanduser().resolve()
|
|
186
|
+
with zipfile.ZipFile(path) as archive:
|
|
187
|
+
names = archive.namelist()
|
|
188
|
+
if len(names) != len(set(names)) or "manifest.json" not in names:
|
|
189
|
+
raise PackError("Invalid or duplicate bundle entries")
|
|
190
|
+
info = archive.getinfo("manifest.json")
|
|
191
|
+
if info.file_size > 32 * 1024 * 1024: raise PackError("Bundle manifest exceeds 32 MiB")
|
|
192
|
+
manifest = json.loads(archive.read("manifest.json")); validate_manifest(manifest)
|
|
193
|
+
refs = references(manifest)
|
|
194
|
+
allowed = {"manifest.json"} | {"objects/" + key for key in refs}
|
|
195
|
+
if set(names) != allowed: raise PackError("Bundle contains missing, unexpected or unsafe paths")
|
|
196
|
+
for key, ref in refs.items():
|
|
197
|
+
info = archive.getinfo("objects/" + key)
|
|
198
|
+
if info.file_size != ref["bytes"] or (info.external_attr >> 16) & 0o170000 == 0o120000:
|
|
199
|
+
raise PackError("Invalid bundle object size/type")
|
|
200
|
+
fd, temp = tempfile.mkstemp(dir=self.root / "objects", prefix=".install-")
|
|
201
|
+
try:
|
|
202
|
+
h = hashlib.sha256(); total = 0
|
|
203
|
+
with archive.open(info) as source, os.fdopen(fd, "wb") as dest:
|
|
204
|
+
while chunk := source.read(1024 * 1024):
|
|
205
|
+
total += len(chunk)
|
|
206
|
+
if total > ref["bytes"]: raise PackError("Oversized bundle object")
|
|
207
|
+
h.update(chunk); dest.write(chunk)
|
|
208
|
+
dest.flush(); os.fsync(dest.fileno())
|
|
209
|
+
if h.hexdigest() != key or total != ref["bytes"]: raise PackError("Bundle object checksum mismatch")
|
|
210
|
+
os.chmod(temp, 0o600)
|
|
211
|
+
target = self.root / "objects" / key
|
|
212
|
+
if target.exists(): self.object(ref)
|
|
213
|
+
else: os.replace(temp, target);sync_directory(target.parent)
|
|
214
|
+
finally:
|
|
215
|
+
if os.path.exists(temp): os.unlink(temp)
|
|
216
|
+
revision = self.save(manifest) if publish else hashlib.sha256(canonical(manifest)).hexdigest()
|
|
217
|
+
return manifest, revision
|
|
218
|
+
|
|
219
|
+
def references(value):
|
|
220
|
+
result = {}
|
|
221
|
+
def walk(v):
|
|
222
|
+
if isinstance(v, dict):
|
|
223
|
+
if "sha256" in v and "bytes" in v:
|
|
224
|
+
key = v["sha256"]
|
|
225
|
+
if key in result and result[key] != v: raise PackError("Conflicting object references")
|
|
226
|
+
result[key] = v
|
|
227
|
+
else:
|
|
228
|
+
for x in v.values(): walk(x)
|
|
229
|
+
elif isinstance(v, list):
|
|
230
|
+
for x in v: walk(x)
|
|
231
|
+
walk(value)
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
def validate_manifest(value):
|
|
235
|
+
if value.get("format") not in {"topolo-agent-pack", "codex-pack"} or value.get("schemaVersion") != 1:
|
|
236
|
+
raise PackError("Unsupported pack format/schema")
|
|
237
|
+
safe_id(value["packId"])
|
|
238
|
+
keys = set()
|
|
239
|
+
for agent in value["agents"]:
|
|
240
|
+
for field in ["pack", "set", "id", "threadId"]: safe_id(agent[field])
|
|
241
|
+
key = (agent["pack"], agent["set"], agent["id"])
|
|
242
|
+
if key in keys: raise PackError(f"Duplicate agent identity: {key}")
|
|
243
|
+
keys.add(key)
|
|
244
|
+
if agent["threadId"] not in value["threads"]: raise PackError("Agent history record missing")
|
|
245
|
+
for tid, thread in value["threads"].items():
|
|
246
|
+
safe_id(tid)
|
|
247
|
+
if thread.get("historyBase") and thread["historyBase"] not in value["threads"]:
|
|
248
|
+
raise PackError(f"Missing inherited history dependency for {tid}")
|
|
249
|
+
for ref in references(value).values():
|
|
250
|
+
if not isinstance(ref["bytes"], int) or ref["bytes"] < 0 or not re.fullmatch(r"[a-f0-9]{64}", ref["sha256"]):
|
|
251
|
+
raise PackError("Invalid content reference")
|
|
252
|
+
|
|
253
|
+
def select(manifest, sets=None, agents=None):
|
|
254
|
+
candidates = manifest["agents"]
|
|
255
|
+
if sets:
|
|
256
|
+
known = {a["set"] for a in candidates}
|
|
257
|
+
for name in sets:
|
|
258
|
+
if name not in known: raise PackError(f"Unknown set {name!r}; available: {', '.join(sorted(known))}")
|
|
259
|
+
candidates = [a for a in candidates if a["set"] in sets]
|
|
260
|
+
if agents:
|
|
261
|
+
chosen = []
|
|
262
|
+
for name in agents:
|
|
263
|
+
matches = [a for a in candidates if name in {a["id"], '/'.join([a['pack'], a['set'], a['id']]), *a.get('aliases', [])}]
|
|
264
|
+
if len(matches) != 1: raise PackError(f"Agent selector {name!r} resolved to {len(matches)} agents; use --set to disambiguate.")
|
|
265
|
+
if matches[0] not in chosen: chosen.append(matches[0])
|
|
266
|
+
candidates = chosen
|
|
267
|
+
if not candidates: raise PackError("Selection contains no agents")
|
|
268
|
+
return candidates
|
|
269
|
+
|
|
270
|
+
def component_key(agent):
|
|
271
|
+
return '/'.join([agent['pack'], agent['set'], agent['id']])
|