Agent status token: 12 h (founder decision 65d8aed4, exception to SI-14 for nk-agent-status)
All checks were successful
Bounded-execution contracts / contracts (push) Successful in 1m30s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

make agent-status-token requests --duration=12h and prints the real expiry from the token's exp claim
(tools/agent-status/token_expiry.py, never the token), since the API server may cap a duration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 943911@bnt-lap001
Assistant-Session: fc322940-1c8e-4bed-ab39-86cec99f23a0
This commit is contained in:
tegwick 2026-11-06 08:02:40 +01:00
parent 619a359072
commit 06291a97da
6 changed files with 87 additions and 5 deletions

View file

@ -212,9 +212,10 @@ bounded-execution-admission-check: ## Validate the example admissions (read-only
execution-attribution-receipt-test: ## Receipt schema v0.1 validator; fixtures match the executor (NK-WP-0056-T15)
python3 -m pytest tools/execution-attribution-receipt/tests
agent-status-token: ## Founder, once per working session: a 1 h read-only token for the agent (NK-WP-0058-T05)
agent-status-token: ## Founder, once per working day: a 12 h read-only token for the agent (NK-WP-0058-T05)
@umask 077 && kubectl --kubeconfig $(HOME)/.kube/config-railiance01 -n sso create token nk-agent-status \
--duration=1h > $(HOME)/.kube/nk-agent-status.token && echo "agent-status-token: valid 1h"
--duration=12h > $(HOME)/.kube/nk-agent-status.token && \
python3 tools/agent-status/token_expiry.py $(HOME)/.kube/nk-agent-status.token
conventions-lint: ## Runbook and tooling conventions from the 2026-11-04/05 pilot (NK-WP-0058-T07), read-only
python3 tools/conventions/lint.py

View file

@ -54,7 +54,8 @@ need, and the agent would still act as ops-mason.
literal secret values (only file paths, references and URLs). `tools/contract-tests/tests/test_agent_status_grant.py`
keeps it that way and keeps the grant read-only and narrow.
**Tokens:** at most 1 hour (SI-14), minted by the founder once per working session. This is read-only, so it isn't a
**Tokens:** 12 hours since 2026-11-06, minted by the founder once per working day. That is an exception to SI-14's
60-minute cap for this read-only identity only (founder decision `65d8aed4`). This is read-only, so it isn't a
warden lane: `make agent-status-token` (founder steps below).
The agent's kubeconfig `~/.kube/config-agent-status` names the cluster and `tokenFile: ~/.kube/nk-agent-status.token`.
@ -97,8 +98,8 @@ strategy, prune and force off. From `~/railiance-platform` after `git pull --ff-
kubectl --kubeconfig $A config use-context nk-agent-status >/dev/null && chmod 600 $A && echo agent-kubeconfig-ok
```
3. **Each working session:** `make agent-status-token` mints a 1 h token into `~/.kube/nk-agent-status.token`
(mode 600) and prints one line.
3. **Each working day:** `make agent-status-token` mints a 12 h token into `~/.kube/nk-agent-status.token`
(mode 600) and prints its real expiry, read from the token's `exp` claim. The API server may cap a duration.
The agent then verifies with `kubectl --kubeconfig ~/.kube/config-agent-status auth can-i --list` and the two
negative checks: Secrets and pod logs in `sso` answer `no`.

View file

@ -0,0 +1,38 @@
"""token_expiry.py on synthetic JWTs (NK-WP-0058-T05): only the expiry is printed."""
import base64
import datetime as dt
import importlib.util
import json
from pathlib import Path
REPO = Path(__file__).resolve().parents[3]
spec = importlib.util.spec_from_file_location('token_expiry', REPO / 'tools/agent-status/token_expiry.py')
te = importlib.util.module_from_spec(spec)
spec.loader.exec_module(te)
def jwt(exp):
enc = lambda d: base64.urlsafe_b64encode(json.dumps(d).encode()).decode().rstrip('=')
return f"{enc({'alg': 'RS256'})}.{enc({'exp': exp, 'sub': 'synthetic'})}.c2lnbmF0dXJl"
def test_prints_expiry_only(tmp_path, capsys):
exp = int((dt.datetime.now(dt.timezone.utc) + dt.timedelta(hours=12)).timestamp())
token = tmp_path / 't'
token.write_text(jwt(exp) + '\n')
assert te.main([str(token)]) == 0
out = capsys.readouterr().out
assert out.startswith('agent-status-token: valid until ')
assert '(12.0 h)' in out or '(11.9 h)' in out
assert jwt(exp) not in out and 'synthetic' not in out
def test_expired_missing_and_malformed_fail(tmp_path, capsys):
old = tmp_path / 'old'
old.write_text(jwt(int(dt.datetime(2020, 1, 1, tzinfo=dt.timezone.utc).timestamp())))
assert te.main([str(old)]) == 1
bad = tmp_path / 'bad'
bad.write_text('not-a-token')
assert te.main([str(bad)]) == 1
assert te.main([str(tmp_path / 'absent')]) == 1
assert 'not-a-token' not in capsys.readouterr().out

View file

@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Print when the agent status token expires, never the token (NK-WP-0058-T05).
python3 tools/agent-status/token_expiry.py ~/.kube/nk-agent-status.token
Reads only the JWT's `exp` claim; the signature and every other claim are ignored and nothing else is printed.
The API server may cap a requested duration, so this reports the expiry the token really has. Exit 1 when the file
is missing, malformed or already expired.
"""
import base64
import datetime as dt
import json
import sys
from pathlib import Path
def expiry(path):
parts = Path(path).read_text().strip().split('.')
if len(parts) != 3:
raise ValueError('not_a_jwt')
payload = parts[1] + '=' * (-len(parts[1]) % 4)
return dt.datetime.fromtimestamp(int(json.loads(base64.urlsafe_b64decode(payload))['exp']), dt.timezone.utc)
def main(argv=None):
path = (argv if argv is not None else sys.argv[1:])[0]
try:
exp = expiry(path)
except (OSError, ValueError, KeyError):
print('agent-status-token: unreadable')
return 1
left = exp - dt.datetime.now(dt.timezone.utc)
hours = left.total_seconds() / 3600
print(f'agent-status-token: valid until {exp:%Y-%m-%d %H:%M}Z ({hours:.1f} h)')
return 0 if hours > 0 else 1
if __name__ == '__main__':
sys.exit(main())

View file

@ -17,6 +17,7 @@ PYTHONDONTWRITEBYTECODE=1 python3 -m pytest -q -rs -p no:cacheprovider \
sso-mfa/k8s/bounded-executor \
tools/contract-tests/tests \
tools/conventions/tests \
tools/agent-status/tests \
"$@" | tee "$out"
if grep -q '^SKIPPED' "$out"; then
echo "contract-test: skipped tests above; every contract must run (are the owner checkouts present?)" >&2

View file

@ -487,6 +487,8 @@ net-kingdom's copy of the grant is deleted: its contract test reads the platform
- Secrets, pod logs, exec, writes, other objects and other namespaces are refused;
- Job, rollout and ExternalSecret status, the live routing, and the informed-decision Application state are
readable.
- **Token lifetime changed to 12 h** (founder, 2026-11-06, decision `65d8aed4`; an exception to SI-14 for this
read-only identity). `make agent-status-token` prints the real expiry.
- **The first reads showed O1 live:** the informed-decision Application pins `005709a` and is `OutOfSync` with git
(`77161fe`). That is reported to railiance-platform.