Skip to content

Commit afd253f

Browse files
hanwenchengclaude
andcommitted
fix(policy): #10 v3 — align all session TTLs to 30 days per wiki/session-token.md
User directive: keep 30-day TTL consistently across the project (shortening agent sessions is a possible future defense-in-depth tweak, but for now uniform with master per the canonical policy in wiki/session-token.md). Reverts the split-by-client-type threat-model row from v2 — now 30 days applies to both master and sandbox/agent cases, matching the wiki declaration. Code alignment (mock server): - handlers/session.rs: introduce DEFAULT_SESSION_TTL_SECONDS const (30 days); replace 3 × 86400u64 + 1 × 3600u64 magic numbers. - test_client.rs: 3 × 86400/3600 → 2_592_000 (30 days). Doc alignment: - wiki/session-token.md:244 — mock default 86400s → 2_592_000s (30 days). - docs/spec/1-step-analysis.md:124 — master auth token "15 min – 24 h" → 30-day canonical policy. - docs/spec/1-step-analysis.md:784 — master session TTL same. - docs/spec/1-step-analysis.md:787 — agent session TTL "4h default, up to 24h" → 30 days (same as master). - wiki/data-classification.md:214 — threat-model row restored to single ~30-day TTL claim. 51 tests across cli + mock-server crates all pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 449ee90 commit afd253f

5 files changed

Lines changed: 22 additions & 13 deletions

File tree

crates/agentkeys-mock-server/src/handlers/session.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ use crate::{
1515
use agentkeys_types::{AuthToken, Scope};
1616
use ed25519_dalek::SigningKey;
1717

18+
/// Session token TTL in seconds — 30 days.
19+
///
20+
/// Canonical AgentKeys policy per `wiki/session-token.md`: the bearer token
21+
/// (master CLI or agent daemon) is a **30-day credential**. Agent/child
22+
/// sessions share the same TTL as master for v0. Shorter TTLs for agent
23+
/// sessions may be introduced later as a defense-in-depth tweak, but they
24+
/// MUST align with the policy doc before being applied here.
25+
const DEFAULT_SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
26+
1827
#[derive(Deserialize)]
1928
pub struct CreateSessionRequest {
2029
pub auth_token: String,
@@ -57,7 +66,7 @@ pub async fn create_session(
5766
db.execute(
5867
"INSERT INTO sessions (token, wallet_address, parent_token, scope_json, created_at, ttl_seconds, revoked)
5968
VALUES (?1, ?2, NULL, NULL, ?3, ?4, 0)",
60-
params![session_token, wallet_address, now, 86400u64],
69+
params![session_token, wallet_address, now, DEFAULT_SESSION_TTL_SECONDS],
6170
)
6271
.map_err(|e| AppError::internal(e.to_string()))?;
6372
return Ok(Json(CreateSessionResponse { session: session_token, wallet: wallet_address }));
@@ -81,7 +90,7 @@ pub async fn create_session(
8190
db.execute(
8291
"INSERT INTO sessions (token, wallet_address, parent_token, scope_json, created_at, ttl_seconds, revoked)
8392
VALUES (?1, ?2, NULL, NULL, ?3, ?4, 0)",
84-
params![session_token, wallet_address, now, 86400u64],
93+
params![session_token, wallet_address, now, DEFAULT_SESSION_TTL_SECONDS],
8594
)
8695
.map_err(|e| AppError::internal(e.to_string()))?;
8796

@@ -144,7 +153,7 @@ pub async fn create_child_session(
144153
db.execute(
145154
"INSERT INTO sessions (token, wallet_address, parent_token, scope_json, created_at, ttl_seconds, revoked)
146155
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0)",
147-
params![child_token, child_wallet, parent.token, scope_json, now, 3600u64],
156+
params![child_token, child_wallet, parent.token, scope_json, now, DEFAULT_SESSION_TTL_SECONDS],
148157
)
149158
.map_err(|e| AppError::internal(e.to_string()))?;
150159

@@ -242,7 +251,7 @@ pub async fn recover_session(
242251
db.execute(
243252
"INSERT INTO sessions (token, wallet_address, parent_token, scope_json, created_at, ttl_seconds, revoked)
244253
VALUES (?1, ?2, NULL, ?3, ?4, ?5, 0)",
245-
params![session_token, wallet_address, scope_json, now, 86400u64],
254+
params![session_token, wallet_address, scope_json, now, DEFAULT_SESSION_TTL_SECONDS],
246255
)
247256
.map_err(|e| AppError::internal(e.to_string()))?;
248257

crates/agentkeys-mock-server/src/test_client.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ impl CredentialBackend for InProcessBackend {
168168
wallet: wallet.clone(),
169169
scope: None,
170170
created_at: 0,
171-
ttl_seconds: 86400,
171+
ttl_seconds: 2_592_000, // 30 days per wiki/session-token.md policy
172172
};
173173
Ok((session, wallet))
174174
}
@@ -197,7 +197,7 @@ impl CredentialBackend for InProcessBackend {
197197
wallet: wallet.clone(),
198198
scope: Some(scope),
199199
created_at: 0,
200-
ttl_seconds: 3600,
200+
ttl_seconds: 2_592_000, // 30 days per wiki/session-token.md policy
201201
};
202202
Ok((session, wallet))
203203
}
@@ -585,7 +585,7 @@ impl CredentialBackend for InProcessBackend {
585585
let session = body["session"].as_object().map(|_| {
586586
let token = body["session"]["token"].as_str().unwrap_or("").to_string();
587587
let wallet = body["session"]["wallet"].as_str().unwrap_or("").to_string();
588-
let ttl = body["session"]["ttl_seconds"].as_u64().unwrap_or(3600);
588+
let ttl = body["session"]["ttl_seconds"].as_u64().unwrap_or(2_592_000);
589589
let created = body["session"]["created_at"].as_u64().unwrap_or(0);
590590
Session {
591591
token,
@@ -650,7 +650,7 @@ impl CredentialBackend for InProcessBackend {
650650
wallet: wallet.clone(),
651651
scope: None,
652652
created_at: 0,
653-
ttl_seconds: 86400,
653+
ttl_seconds: 2_592_000, // 30 days per wiki/session-token.md policy
654654
};
655655
Ok((session, wallet))
656656
}

docs/spec/1-step-analysis.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ AgentKeys' answer is structurally different from 1Password: **we don't hand user
121121

122122
| Tier | Lifetime | Storage (original spec) | Storage (corrected, JWT model) | Usage |
123123
| --------------------- | ---------------------------------------------------------------- | ------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
124-
| **Master auth token** | Short (15 min – 24 h, configurable via `AuthOptions.expires_at`) | OS keychain | Plain file or env var (JWT string, not a private key) | Management commands: `agentkeys init`, `store`, `usage`, `teardown`, `approve`. Never used by running agents. |
124+
| **Master auth token** | 30 days (canonical AgentKeys policy per `wiki/session-token.md`; `AuthOptions.expires_at` can shorten per-session) | OS keychain | Plain file or env var (JWT string, not a private key) | Management commands: `agentkeys init`, `store`, `usage`, `teardown`, `approve`. Never used by running agents. |
125125
| **Agent auth token** | Long (hours to days) | Sandbox filesystem (`~/.agentkeys/session`, 0600) | Same (JWT string in file, 0600) | MCP Credential Server authentication. Scoped to specific credentials for a specific agent. |
126126

127127

@@ -781,10 +781,10 @@ This section explicitly reconciles any points where earlier rounds of this sub-i
781781
| **Canonical account name (Round 6)** | **x402 wallet address (EVM), minted in Heima TEE on account creation. Same primary key for master and each child.** |
782782
| **Billing model (Round 6)** | **Each account's wallet holds its own USDC. Master funds children. Empty wallet = agent stops. No on-chain spend-limit code needed — the balance IS the limit.** |
783783
| Master session storage | OS keychain (Keychain Services / Credential Manager / libsecret), biometric-gated |
784-
| Master session TTL | Short (15 min - 24 h idle, 1P/Enpass style) |
784+
| Master session TTL | 30 days (canonical AgentKeys policy per `wiki/session-token.md`) |
785785
| **Agent session storage** | **On stock sandbox: `/home/gem/.agentkeys/session`** (mode 0600, owner gem) + memfd_secret runtime pages + seccomp-bpf process restrictions + daemon with Unix socket (ssh-agent model). **On cloud LLM or custom sandbox: `$HOME/.agentkeys/session`** with the same hardening stack. *(Original Round 6 design specified `/var/lib/agentkeys/session` with dedicated UID + LSM + Landlock — see §3.3a for historical reference, §3.3c for what ships.)* |
786786
| **Storage stack order (Round 6)** | **S1 (this Round 6 hardening) → S2 (rolling ratchet) → S3 (provider attestation). S4 and S5 rejected.** |
787-
| Agent session TTL | Long (4 h default, up to 24 h for v0) |
787+
| Agent session TTL | 30 days (same policy as master CLI per `wiki/session-token.md`; may be shortened in a future defense-in-depth tweak) |
788788
| Scope | Each agent session bound to its specific service credentials only |
789789
| Revocation | Instant via master CLI (`agentkeys revoke 0x...`) |
790790
| Recovery | New sandbox runs `agentkeys pair` → master runs `agentkeys approve <pair-code>` (mints new session for same wallet address). *(Original design used `agentkeys attach agent-A` with direct HTTP push — superseded by rendezvous model.)* |

wiki/data-classification.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ Daemon stores session locally (session token or session file, mode 0600) [CLIENT
211211
| Compromise point | What they get | What they DON'T get | Blast radius |
212212
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
213213
| **Chain data exfiltration** (read all on-chain state) | All plaintext: addresses, identity hashes, scopes, audit events, pair metadata. Credential ciphertext (unreadable without shielding key). | Any private key. Any plaintext credential. Session tokens (not on chain). Original identity info (only hash). | Information disclosure only. No ability to decrypt credentials or impersonate users. |
214-
| **Client device compromise** (laptop/sandbox) | Session token (bearer credential). Possibly plaintext credential in memory if timed during a read operation. | Any TEE key. Other users' data. Credential ciphertext. | Impersonate this user until the bearer expires or is revoked (~6s). TTL depends on WHICH client was compromised: master CLI bearer ~30 days; sandbox daemon agent/child sessions are short-lived (1–24h; 4h default). If credential was in memory, one credential for one service exposed. |
214+
| **Client device compromise** (laptop/sandbox) | Session token (bearer credential). Possibly plaintext credential in memory if timed during a read operation. | Any TEE key. Other users' data. Credential ciphertext. | Impersonate this user until session token expires (~30 days) or is revoked (~6s). If credential was in memory, one credential for one service exposed. |
215215
| **Session token theft** | Impersonate the user for the token's remaining TTL. Scoped by the token's `sub` (one user) + on-chain scope (specific services). | TEE keys. Other users' sessions. Ability to forge new tokens. Ability to sign extrinsics (TEE signs, not the client). | Bounded by TTL + scope. Revocable via on-chain revocation list (~6s). |
216216
| **TEE compromise** (enclave breach, side-channel, insider) | All sealed keys (shielding, RSA, wallet/MSK). Can decrypt ALL credential blobs. Can forge session tokens. Can sign extrinsics as any user. | Chain history (already written, immutable). Can't rewrite past audit events. | **Total.** All users, all credentials, all operations. Recovery: rotate shielding key, re-encrypt all credentials, rotate MSK, re-issue all session tokens. The on-chain audit trail survives — forensic investigation of what happened during the breach is possible from chain data. |
217217
| **Paymaster compromise** (treasury drained) | Can stop paying for audit extrinsic submission. Existing credentials and sessions unaffected. | Any key. Any credential. Any ability to impersonate. | Audit events stop appearing on chain. Credential reads still work (TEE serves from chain state). Degraded mode: reads work, audit is paused. |

wiki/session-token.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ After revocation, the agent's session token is cryptographically valid (the RSA
241241
| Token format | Random 32-byte hex string (opaque bearer) | Signed JWT (RSA, with claims) |
242242
| Token issuer | Mock backend (`generate_token()`) | Heima TEE (`jwt::create(&claims, private_key)`) |
243243
| Verification | Bearer lookup in SQLite `sessions` table | Stateless RSA signature check (no table) |
244-
| Expiration | TTL field in SQLite (86400s default) | `exp` claim in JWT (configurable, target 30 days) |
244+
| Expiration | TTL field in SQLite (2_592_000s = 30 days default) | `exp` claim in JWT (configurable, target 30 days) |
245245
| Revocation | `UPDATE sessions SET revoked=1` in SQLite | On-chain revocation list, ~6s propagation |
246246
| Storage (master) | OS keychain via `keyring-rs` | OS keychain (same, storing JWT string instead of random token) |
247247
| Storage (daemon) | File fallback at `~/.agentkeys/session.json` | File at `~/.agentkeys/token` (mode 0600) |

0 commit comments

Comments
 (0)