@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.
Files changed (123) hide show
  1. package/README.md +45 -0
  2. package/agent-packs/README.md +495 -0
  3. package/agent-packs/pyproject.toml +16 -0
  4. package/agent-packs/requirements-test.txt +1 -0
  5. package/agent-packs/src/topolo_agent_pack/__init__.py +1 -0
  6. package/agent-packs/src/topolo_agent_pack/blueprint.py +57 -0
  7. package/agent-packs/src/topolo_agent_pack/claude.py +265 -0
  8. package/agent-packs/src/topolo_agent_pack/cli.py +443 -0
  9. package/agent-packs/src/topolo_agent_pack/execution.py +154 -0
  10. package/agent-packs/src/topolo_agent_pack/fresh.py +158 -0
  11. package/agent-packs/src/topolo_agent_pack/live.py +403 -0
  12. package/agent-packs/src/topolo_agent_pack/native.py +645 -0
  13. package/agent-packs/src/topolo_agent_pack/policy.py +92 -0
  14. package/agent-packs/src/topolo_agent_pack/storage.py +271 -0
  15. package/agent-packs/src/topolo_agent_pack/topolo.py +321 -0
  16. package/agent-packs/templates/career/AGENTS.md +17 -0
  17. package/agent-packs/templates/career/README.md +14 -0
  18. package/agent-packs/templates/career/agents/intakes/career-intake/AGENTS.md +21 -0
  19. package/agent-packs/templates/career/agents/specialists/application-preparation/AGENTS.md +21 -0
  20. package/agent-packs/templates/career/agents/specialists/application-tracker/AGENTS.md +21 -0
  21. package/agent-packs/templates/career/agents/specialists/interview-coach/AGENTS.md +21 -0
  22. package/agent-packs/templates/career/agents/specialists/opportunity-research/AGENTS.md +21 -0
  23. package/agent-packs/templates/career/agents/specialists/profile-cv/AGENTS.md +21 -0
  24. package/agent-packs/templates/career/pack.json +124 -0
  25. package/agent-packs/templates/career/shared/PACK.md +22 -0
  26. package/agent-packs/templates/career/shared/TOPOLO.md +13 -0
  27. package/agent-packs/templates/career/shared/WORKFLOW.md +13 -0
  28. package/agent-packs/templates/career/skills/topolo/SKILL.md +305 -0
  29. package/agent-packs/templates/clients/AGENTS.md +17 -0
  30. package/agent-packs/templates/clients/README.md +14 -0
  31. package/agent-packs/templates/clients/agents/intakes/client-intake/AGENTS.md +21 -0
  32. package/agent-packs/templates/clients/agents/specialists/acceptance-review/AGENTS.md +21 -0
  33. package/agent-packs/templates/clients/agents/specialists/delivery-planner/AGENTS.md +21 -0
  34. package/agent-packs/templates/clients/agents/specialists/discovery/AGENTS.md +21 -0
  35. package/agent-packs/templates/clients/agents/specialists/proposal-writer/AGENTS.md +21 -0
  36. package/agent-packs/templates/clients/agents/specialists/solution-design/AGENTS.md +21 -0
  37. package/agent-packs/templates/clients/pack.json +126 -0
  38. package/agent-packs/templates/clients/shared/PACK.md +22 -0
  39. package/agent-packs/templates/clients/shared/TOPOLO.md +13 -0
  40. package/agent-packs/templates/clients/shared/WORKFLOW.md +13 -0
  41. package/agent-packs/templates/clients/skills/topolo/SKILL.md +305 -0
  42. package/agent-packs/templates/creative/AGENTS.md +17 -0
  43. package/agent-packs/templates/creative/README.md +14 -0
  44. package/agent-packs/templates/creative/agents/intakes/creative-director/AGENTS.md +21 -0
  45. package/agent-packs/templates/creative/agents/specialists/brand-copy/AGENTS.md +21 -0
  46. package/agent-packs/templates/creative/agents/specialists/media-production/AGENTS.md +21 -0
  47. package/agent-packs/templates/creative/agents/specialists/quality-review/AGENTS.md +21 -0
  48. package/agent-packs/templates/creative/agents/specialists/storytelling/AGENTS.md +21 -0
  49. package/agent-packs/templates/creative/agents/specialists/visual-design/AGENTS.md +21 -0
  50. package/agent-packs/templates/creative/pack.json +128 -0
  51. package/agent-packs/templates/creative/shared/PACK.md +22 -0
  52. package/agent-packs/templates/creative/shared/TOPOLO.md +13 -0
  53. package/agent-packs/templates/creative/shared/WORKFLOW.md +13 -0
  54. package/agent-packs/templates/creative/skills/topolo/SKILL.md +305 -0
  55. package/agent-packs/templates/founder/AGENTS.md +17 -0
  56. package/agent-packs/templates/founder/README.md +14 -0
  57. package/agent-packs/templates/founder/agents/intakes/founder-intake/AGENTS.md +21 -0
  58. package/agent-packs/templates/founder/agents/specialists/commercial-modelling/AGENTS.md +21 -0
  59. package/agent-packs/templates/founder/agents/specialists/customer-research/AGENTS.md +21 -0
  60. package/agent-packs/templates/founder/agents/specialists/decision-review/AGENTS.md +21 -0
  61. package/agent-packs/templates/founder/agents/specialists/partnerships/AGENTS.md +21 -0
  62. package/agent-packs/templates/founder/agents/specialists/strategy/AGENTS.md +21 -0
  63. package/agent-packs/templates/founder/pack.json +126 -0
  64. package/agent-packs/templates/founder/shared/PACK.md +22 -0
  65. package/agent-packs/templates/founder/shared/TOPOLO.md +13 -0
  66. package/agent-packs/templates/founder/shared/WORKFLOW.md +13 -0
  67. package/agent-packs/templates/founder/skills/topolo/SKILL.md +305 -0
  68. package/agent-packs/templates/learning/AGENTS.md +17 -0
  69. package/agent-packs/templates/learning/README.md +14 -0
  70. package/agent-packs/templates/learning/agents/intakes/learning-intake/AGENTS.md +21 -0
  71. package/agent-packs/templates/learning/agents/specialists/critical-reviewer/AGENTS.md +21 -0
  72. package/agent-packs/templates/learning/agents/specialists/curriculum-designer/AGENTS.md +21 -0
  73. package/agent-packs/templates/learning/agents/specialists/knowledge-steward/AGENTS.md +21 -0
  74. package/agent-packs/templates/learning/agents/specialists/practice-coach/AGENTS.md +21 -0
  75. package/agent-packs/templates/learning/agents/specialists/tutor/AGENTS.md +21 -0
  76. package/agent-packs/templates/learning/pack.json +124 -0
  77. package/agent-packs/templates/learning/shared/PACK.md +22 -0
  78. package/agent-packs/templates/learning/shared/TOPOLO.md +13 -0
  79. package/agent-packs/templates/learning/shared/WORKFLOW.md +13 -0
  80. package/agent-packs/templates/learning/skills/topolo/SKILL.md +305 -0
  81. package/agent-packs/templates/personal/AGENTS.md +17 -0
  82. package/agent-packs/templates/personal/README.md +14 -0
  83. package/agent-packs/templates/personal/agents/intakes/personal-intake/AGENTS.md +21 -0
  84. package/agent-packs/templates/personal/agents/specialists/correspondence/AGENTS.md +21 -0
  85. package/agent-packs/templates/personal/agents/specialists/document-assistant/AGENTS.md +21 -0
  86. package/agent-packs/templates/personal/agents/specialists/planning/AGENTS.md +21 -0
  87. package/agent-packs/templates/personal/agents/specialists/research/AGENTS.md +21 -0
  88. package/agent-packs/templates/personal/agents/specialists/travel-logistics/AGENTS.md +21 -0
  89. package/agent-packs/templates/personal/pack.json +125 -0
  90. package/agent-packs/templates/personal/shared/PACK.md +22 -0
  91. package/agent-packs/templates/personal/shared/TOPOLO.md +13 -0
  92. package/agent-packs/templates/personal/shared/WORKFLOW.md +13 -0
  93. package/agent-packs/templates/personal/skills/topolo/SKILL.md +305 -0
  94. package/agent-packs/templates/research/AGENTS.md +17 -0
  95. package/agent-packs/templates/research/README.md +14 -0
  96. package/agent-packs/templates/research/agents/intakes/research-intake/AGENTS.md +21 -0
  97. package/agent-packs/templates/research/agents/specialists/adversarial-review/AGENTS.md +21 -0
  98. package/agent-packs/templates/research/agents/specialists/data-analysis/AGENTS.md +21 -0
  99. package/agent-packs/templates/research/agents/specialists/source-research/AGENTS.md +21 -0
  100. package/agent-packs/templates/research/agents/specialists/synthesis/AGENTS.md +21 -0
  101. package/agent-packs/templates/research/agents/specialists/technical-research/AGENTS.md +21 -0
  102. package/agent-packs/templates/research/pack.json +124 -0
  103. package/agent-packs/templates/research/shared/PACK.md +22 -0
  104. package/agent-packs/templates/research/shared/TOPOLO.md +13 -0
  105. package/agent-packs/templates/research/shared/WORKFLOW.md +13 -0
  106. package/agent-packs/templates/research/skills/topolo/SKILL.md +305 -0
  107. package/agent-packs/templates/ventures/AGENTS.md +17 -0
  108. package/agent-packs/templates/ventures/README.md +14 -0
  109. package/agent-packs/templates/ventures/agents/intakes/venture-intake/AGENTS.md +21 -0
  110. package/agent-packs/templates/ventures/agents/specialists/customer-discovery/AGENTS.md +21 -0
  111. package/agent-packs/templates/ventures/agents/specialists/economics/AGENTS.md +21 -0
  112. package/agent-packs/templates/ventures/agents/specialists/experiment-design/AGENTS.md +21 -0
  113. package/agent-packs/templates/ventures/agents/specialists/market-research/AGENTS.md +21 -0
  114. package/agent-packs/templates/ventures/agents/specialists/product-concept/AGENTS.md +21 -0
  115. package/agent-packs/templates/ventures/pack.json +126 -0
  116. package/agent-packs/templates/ventures/shared/PACK.md +22 -0
  117. package/agent-packs/templates/ventures/shared/TOPOLO.md +13 -0
  118. package/agent-packs/templates/ventures/shared/WORKFLOW.md +13 -0
  119. package/agent-packs/templates/ventures/skills/topolo/SKILL.md +305 -0
  120. package/agent-packs/topolo-agent-pack +8 -0
  121. package/dist/claude-pack-backend-IDQQX3DG.js +555 -0
  122. package/dist/index.js +143 -58
  123. 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']])