arkaos 4.47.0 → 4.48.0

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 (39) hide show
  1. package/THE-ARKAOS-GUIDE.md +1 -1
  2. package/VERSION +1 -1
  3. package/core/egress/policy.py +84 -32
  4. package/core/kb/__init__.py +9 -0
  5. package/core/kb/nlm_client.py +883 -0
  6. package/harness/codex/AGENTS.md +1 -1
  7. package/harness/copilot/copilot-instructions.md +1 -1
  8. package/harness/cursor/rules/arkaos.mdc +2 -2
  9. package/harness/gemini/GEMINI.md +1 -1
  10. package/harness/opencode/AGENTS.md +1 -1
  11. package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
  12. package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
  13. package/harness/opencode/agents/arka-cfo-helena.md +1 -1
  14. package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
  15. package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
  16. package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
  17. package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
  18. package/harness/opencode/agents/arka-coo-sofia.md +1 -1
  19. package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
  20. package/harness/opencode/agents/arka-cqo-marta.md +1 -1
  21. package/harness/opencode/agents/arka-cto-marco.md +1 -1
  22. package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
  23. package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
  24. package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
  25. package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
  26. package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
  27. package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
  28. package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
  29. package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
  30. package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
  31. package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
  32. package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
  33. package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
  34. package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
  35. package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
  36. package/harness/zed/.rules +1 -1
  37. package/knowledge/skills-manifest.json +1 -1
  38. package/package.json +1 -1
  39. package/pyproject.toml +1 -1
@@ -1,6 +1,6 @@
1
1
  # The ArkaOS Guide
2
2
 
3
- > v4.47.0 — 89 agents, 17 departments, 332 skills, 297 commands, 19 ADRs.
3
+ > v4.48.0 — 89 agents, 17 departments, 332 skills, 297 commands, 19 ADRs.
4
4
  > One file, everything you need to start. Generated by `scripts/guide_gen.py` — never hand-edited.
5
5
 
6
6
  ## What it is
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.47.0
1
+ 4.48.0
@@ -28,6 +28,7 @@ import re
28
28
  from dataclasses import dataclass, field
29
29
  from datetime import datetime
30
30
  from pathlib import Path
31
+ from typing import Any
31
32
 
32
33
  from core.egress import allowlist, audit, redact
33
34
  from core.governance.harness_scanner import secret_labels
@@ -42,7 +43,7 @@ class Finding:
42
43
  # # | audit-unavailable | payload-not-text | guard-failure
43
44
  token: str
44
45
 
45
- def to_audit(self, salt: bytes = b"") -> dict:
46
+ def to_audit(self, salt: bytes = b"") -> dict[str, Any]:
46
47
  return {
47
48
  "kind": self.kind,
48
49
  "token_sha16": (
@@ -64,7 +65,7 @@ class EgressDecision:
64
65
  redacted_sha256: str = ""
65
66
  audited: bool = False
66
67
 
67
- def to_audit(self, salt: bytes = b"") -> dict:
68
+ def to_audit(self, salt: bytes = b"") -> dict[str, Any]:
68
69
  return {
69
70
  "allowed": self.allowed,
70
71
  "destination": self.destination,
@@ -99,26 +100,43 @@ def evaluate(
99
100
  ) -> EgressDecision:
100
101
  """Judge one payload against the policy. Never raises.
101
102
 
102
- ``home`` scopes the home-path check and the default allowlist /
103
- audit locations; ``now`` pins allowlist expiry for tests.
103
+ ``home`` scopes the home-path check, the redaction config and the
104
+ default allowlist / audit locations; ``now`` pins allowlist expiry
105
+ for tests.
104
106
  """
105
107
  destination = _safe_str(destination)
106
- # Digest computed ONCE, before any failure path, so no handler
107
- # ever re-executes the operation that failed (QG D1 r2 E-B1).
108
- payload_digest = _sha256(text) if isinstance(text, str) else ""
108
+ # ONCE, before any failure path, so no handler re-runs what
109
+ # failed. Named `digest`: `payload_digest` shadowed the public
110
+ # function here (QG D2 r12, Francisca M2).
111
+ digest = payload_digest(text) if isinstance(text, str) else ""
109
112
  try:
110
113
  decision = _judge(
111
114
  text, destination, config_path, home, allowlist_path, now
112
115
  )
113
116
  except Exception as exc: # never-raises boundary — deny, not crash
114
- decision = EgressDecision(
115
- allowed=False, destination=destination,
116
- payload_sha256=payload_digest,
117
- findings=[Finding("guard-failure", type(exc).__name__)],
118
- )
117
+ decision = _guard_failure(destination, digest, exc)
119
118
  return _audited(decision, home, audit_path, now)
120
119
 
121
120
 
121
+ def _guard_failure(
122
+ destination: str, digest: str, exc: BaseException
123
+ ) -> EgressDecision:
124
+ return EgressDecision(
125
+ allowed=False, destination=destination, payload_sha256=digest,
126
+ findings=[Finding("guard-failure", type(exc).__name__)],
127
+ )
128
+
129
+
130
+ def default_redaction_config_path(home: Path | None = None) -> Path:
131
+ """The identifier list *home* implies.
132
+
133
+ Mirrors ``leak_scanner._DEFAULT_CONFIG_PATH`` for the real home, so
134
+ scoping by ``home`` never changes production behaviour — pinned by
135
+ ``test_egress_policy.py`` rather than by this comment.
136
+ """
137
+ return (home or Path.home()) / ".arkaos" / "redaction-clients.json"
138
+
139
+
122
140
  def _safe_str(value: object) -> str:
123
141
  try:
124
142
  return str(value)
@@ -150,7 +168,7 @@ def _audited(
150
168
  return decision
151
169
 
152
170
 
153
- def enforce(text: object, destination: str, **kwargs) -> str:
171
+ def enforce(text: object, destination: str, **kwargs: Any) -> str:
154
172
  """The redacted text cleared to leave, or :class:`EgressDeniedError`."""
155
173
  decision = evaluate(text, destination, **kwargs)
156
174
  if not decision.allowed or decision.redacted_text is None:
@@ -167,41 +185,70 @@ def _judge(
167
185
  now: datetime | None,
168
186
  ) -> EgressDecision:
169
187
  if not isinstance(text, str):
170
- return EgressDecision(
171
- allowed=False, destination=destination, payload_sha256="",
172
- findings=[Finding("payload-not-text", type(text).__name__)],
173
- )
188
+ return _not_text(destination, text)
174
189
  decision = EgressDecision(
175
190
  allowed=False, destination=destination,
176
- payload_sha256=_sha256(text),
191
+ payload_sha256=payload_digest(text),
177
192
  )
178
- clean, failure = _redacted(text, config_path)
179
- if failure is not None:
180
- decision.findings.append(failure)
193
+ # One scoped path for both layers: scoping only the redaction call
194
+ # left residual_identifiers reading the real machine's list
195
+ # (QG D2 r3, Francisca B1).
196
+ scoped = _scoped_config(config_path, home)
197
+ clean = _redacted(text, scoped)
198
+ if isinstance(clean, Finding):
199
+ decision.findings.append(clean)
181
200
  return decision
182
201
  _collect_findings(
183
- decision, clean, destination, config_path, home, allowlist_path, now
202
+ decision, clean, destination, scoped, home, allowlist_path, now
184
203
  )
185
204
  if not decision.findings:
186
205
  decision.allowed = True
187
206
  decision.redacted_text = clean
188
- decision.redacted_sha256 = _sha256(clean)
207
+ decision.redacted_sha256 = payload_digest(clean)
189
208
  return decision
190
209
 
191
210
 
192
- def _sha256(text: str) -> str:
211
+ def _not_text(destination: str, text: object) -> EgressDecision:
212
+ return EgressDecision(
213
+ allowed=False, destination=destination, payload_sha256="",
214
+ findings=[Finding("payload-not-text", type(text).__name__)],
215
+ )
216
+
217
+
218
+ def payload_digest(text: str) -> str:
193
219
  """Total over any str — surrogatepass, because a payload holding a
194
220
  lone surrogate (routine from errors="surrogateescape" decoding or
195
221
  json.loads of an escape) must be DIGESTIBLE to be denied with an
196
- audit line (QG D1 r2 E-B1)."""
222
+ audit line (QG D1 r2 E-B1).
223
+
224
+ Public because callers outside this package need the same digest
225
+ the audit trail records; reaching for a private symbol made a D1
226
+ rename a silent runtime break downstream (QG D2 r1, Francisca M6).
227
+ """
197
228
  payload = text.encode("utf-8", errors="surrogatepass")
198
229
  return hashlib.sha256(payload).hexdigest()
199
230
 
200
231
 
201
- def _redacted(
202
- text: str, config_path: Path | None
203
- ) -> tuple[str | None, Finding | None]:
204
- """``(clean_text, None)`` or ``(None, denial_finding)``.
232
+ def _scoped_config(config_path: Path | None, home: Path | None) -> Path | None:
233
+ """The redaction config *home* implies, unless one was named.
234
+
235
+ ``home`` scoped ``allowlist_path`` and ``audit_path`` but NOT the
236
+ redaction config, so a caller passing ``home=`` and forgetting
237
+ ``config_path`` judged paths against one home while redacting
238
+ against another (QG D2 r1, Francisca M7).
239
+
240
+ Called from inside ``_judge``, i.e. inside ``evaluate``'s try: the
241
+ same defaulting one frame up sat OUTSIDE it, and a non-Path home
242
+ turned the never-raises boundary into a TypeError — the very shape
243
+ QG D1 r2 F-M5 already closed once.
244
+ """
245
+ if config_path is not None or home is None:
246
+ return config_path
247
+ return default_redaction_config_path(home)
248
+
249
+
250
+ def _redacted(text: str, config_path: Path | None) -> str | Finding:
251
+ """The clean text, or the finding that denies it.
205
252
 
206
253
  A redaction that CRASHES proves nothing about the payload — same
207
254
  posture as a missing config: denied, never allowlistable (QG D1
@@ -209,11 +256,16 @@ def _redacted(
209
256
  """
210
257
  try:
211
258
  clean, _counts = redact.redact(text, config_path)
212
- return clean, None
259
+ # Annotated: redact() is untyped, so `clean` arrives as Any and
260
+ # the union return silently degrades to Any (mypy no-any-return,
261
+ # surfaced only with --follow-imports=skip —
262
+ # QG D2 r3, Francisca M3).
263
+ clean_text: str = clean
264
+ return clean_text
213
265
  except redact.SanitizerConfigMissing:
214
- return None, Finding("redaction-config-missing", "")
266
+ return Finding("redaction-config-missing", "")
215
267
  except Exception as exc:
216
- return None, Finding("redaction-failed", type(exc).__name__)
268
+ return Finding("redaction-failed", type(exc).__name__)
217
269
 
218
270
 
219
271
  def _collect_findings(
@@ -0,0 +1,9 @@
1
+ """Knowledge-base integrations that reach outside the machine.
2
+
3
+ Everything here goes through ``core.egress.policy`` before a byte
4
+ leaves. The package is deliberately absent from the critical path:
5
+ ``tests/python/test_notebooklm_chokepoint.py`` asserts by import graph
6
+ that ``core/governance``, ``core/workflow`` and ``core/release`` never
7
+ import it, so an upstream tool breaking can stall research but never a
8
+ gate or a release.
9
+ """