CR ID: NPS-CR-0003
Target version: v1.0-alpha.6
Status: Accepted
Accepted: 2026-05-11
Type: Backward-compatible extension (new identifier conventions, new IdentFrame field, new CA endpoints, new revocation reason)
Author: Ori, LabAcacia
Affected components: NIP spec (NPS-3), NIP error codes, .NET SDK (NPS.NIP), nip-ca-server, conformance tests, public docs
NPS today issues one NID per identity. An orchestrator that runs many short-lived task executions has only two unsatisfactory options:
This CR adds a first-class group / session NID hierarchy:
The full trust chain is:
human owner → Operator key (NIP §2.1) → orchestrator group NID → short-lived session NID
Existing single-NID flows (POST /v1/agents/register, etc.) are unchanged and remain the default. Group / session is opt-in.
The orchestrator-with-many-short-tasks pattern is dominant in production AI Agent deployments — a long-running planner spawns dozens to thousands of isolated tool-call or subtask executions per hour. Without a group / session split:
register call. Operator API keys grow into hot, broadly-shared secrets — exactly what NIP §10 was designed to avoid.The pattern is well-precedented:
NIP needs the equivalent at the NID layer.
Add to spec/NPS-3-NIP.md §3 NID Format, after the examples:
Reserved identifier prefixes (NPS-CR-0003)
The following identifier prefixes are reserved on NIDs of
entity-type = agentand signal a structural role within the orchestrator / session lineage model. They MUST follow the existing identifier ABNF (1*(ALPHA / DIGIT / "-" / "_" / ".")).
Prefix Role Example group-Orchestrator group NID. The trust anchor for a fleet of session NIDs issued by the CA on the group’s behalf. urn:nps:agent:ca.example.com:group-7f3c9e1a-b2d8-4c6f-9a01-...session-Short-lived session NID issued under a group. The portion after session-MUST be a unique identifier of the form{unix-timestamp}-{random}where{random}is at least 8 hex characters.urn:nps:agent:ca.example.com:session-1714672800-f3a92c0bIdentifiers without these prefixes continue to behave as ordinary agent NIDs. Receivers MUST NOT reject a NID solely on its prefix; the prefix is informational and the authoritative role is carried in
IdentFrame.lineage.role(§5.1.3).
entity-type is not extended — group and session NIDs remain agent so that pre-CR-0003 verifiers parse and route them as ordinary agents (safe degradation: they cannot exploit lineage but cannot be tricked by it either). The protocol-level nop:orchestrate capability (NPS-5) continues to express the role of being an orchestrator and remains independent of this CR.
Add a new optional top-level field to IdentFrame:
"lineage": {
"role": "group" | "session",
"parent_nid": "urn:nps:agent:...",
"group_nid": "urn:nps:agent:...",
"session_id": "session-1714672800-f3a92c0b",
"purpose": "data-extraction-job-42",
"owner_user_id": "user-123",
"owner_key_id": "kid-456"
}
| Sub-field | Type | Required | Description |
|---|---|---|---|
role |
enum | required when lineage is present |
"group" for an orchestrator group NID; "session" for a short-lived session NID. |
parent_nid |
string (NID) | required when role = "session" |
The immediate parent NID. For a 1-level chain (group → session), equals group_nid. |
group_nid |
string (NID) | required when role = "session" |
The group NID at the root of the chain. Equals parent_nid for a 1-level chain. Reserved for future deeper chains. |
session_id |
string | required when role = "session" |
Stable id of this execution; matches the session-... portion of the NID identifier. Echoes back to allow indexing without parsing the URN. |
purpose |
string | optional | Free-form, human-readable label for what this execution is for (e.g. "order-classification-job"). Bounded to 256 UTF-8 bytes. |
owner_user_id |
string | optional | Stable identifier of the human owner this group is acting on behalf of (e.g. an internal user UUID). Required when role = "group" and the deployment knows the owner. |
owner_key_id |
string | optional | kid hint identifying the owner-key (Operator key, OIDC sub, hardware-token id) that authorized creation of the group. |
Signature rules: lineage is part of the signed canonical JSON (unlike metadata, which is excluded). Any modification to lineage invalidates the IdentFrame signature. This is what makes the chain verifiable — a Node can check that a session NID’s lineage.parent_nid was authoritatively asserted by the CA, not bolted on later.
Backward compatibility: pre-CR-0003 publishers omit lineage; pre-CR-0003 verifiers are strict-canonical (sort + omit signature / metadata / cert_format / cert_chain) and therefore drop unknown fields if their DTO does not carry them, in which case signature verification of a CR-0003 frame will fail. This matches the phase model used for NPS-RFC-0003 assurance_level: new senders aware of CR-0003 produce CR-0003 frames; old verifiers MUST be upgraded to verify them. The default identity mint (regular agent NIDs) does not include lineage and remains exactly bit-compatible.
parent_revoked (NPS-3 §5.3)Extend the RevokeFrame.reason enum:
| Reason | When |
|---|---|
parent_revoked (new) |
Set on a session-NID RevokeFrame that the CA emits as part of cascade revocation when the session’s group NID was revoked. Distinguishes a session that was forcibly invalidated by ancestry from one revoked on its own merits. |
The five existing reasons (key_compromise / ca_compromise / affiliation_changed / superseded / cessation_of_operation) are unchanged.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /v1/orchestrators/groups/register |
Operator API key | Register an orchestrator group; returns the group’s IdentFrame with lineage.role = "group". |
| POST | /v1/orchestrators/groups/{group_nid}/sessions/issue |
Group JWS or Operator API key | Issue a short-lived session NID under the group. Returns the session’s IdentFrame with lineage.role = "session". |
| POST | /v1/orchestrators/groups/{group_nid}/revoke |
Operator API key | Revoke the group AND cascade-revoke every live session whose lineage.group_nid equals this group. |
| GET | /v1/orchestrators/groups/{group_nid}/sessions |
Operator API key | List sessions issued under this group (audit). |
Discovery (/.well-known/nps-ca) gains a capabilities entry "orchestrator-group" so clients can detect support.
When an orchestrator authenticates with its group key (no Operator credential), the request body MUST be a flattened JWS with these protected-header fields and a JSON payload:
// Protected header (b64url-encoded)
{
"alg": "EdDSA",
"kid": "<group_nid>",
"nps-purpose": "session-issue"
}
// Payload (b64url-encoded)
{
"session_pub_key": "ed25519:...", // session keypair public half
"purpose": "data-extraction", // optional, ≤256 bytes
"validity_seconds": 3600, // optional; clamped to [60, MaxValidity]
"scope_json": {...}, // optional; MUST NOT exceed group scope
"iat": 1714672800 // unix seconds, ±MaxClockSkew
}
// Outer JWS object (Content-Type: application/jose+json)
{
"protected": "<b64url(header)>",
"payload": "<b64url(payload)>",
"signature": "<b64url(Ed25519 signature)>"
}
The CA MUST:
alg = EdDSA and nps-purpose = session-issue.kid to a stored group NID; reject NIP-CA-PARENT-NOT-FOUND if not found, NIP-CA-PARENT-NOT-GROUP if the NID exists but lineage.role ≠ "group", NIP-CA-GROUP-REVOKED if revoked.NIP-CA-JWS-INVALID on failure.NIP-CA-JWS-EXPIRED if |now − iat| > MaxClockSkew (default 5 minutes).NIP-CA-SESSION-VALIDITY-INVALID if validity_seconds is outside the configured [1 minute, MaxSessionValidity] band.scope_json is supplied, verify it is a non-strict subset of the group’s scope (no scope expansion per NIP §10.3).When an Operator API key is presented (Authorization: Bearer ...), the body is plain JSON (no JWS wrapper). This path exists for break-glass / tooling and SHOULD be locked down by deployment-side rate limits.
Insert a step 3a in spec/NPS-3-NIP.md §7 between the existing signature check (3) and OCSP lookup (4):
3a. If the IdentFrame carries
lineage.parent_nid, the verifier MUST OCSP-lookup the parent NID and rejectNIP-CERT-PARENT-REVOKEDif the parent is revoked or expired. The lookup is mandatory regardless of whether the session NID itself is still valid.
This is the verify-side half of cascade revocation; combined with the CA-side cascade in §3.3 it provides defense-in-depth: a misconfigured CA that fails to record a cascade still gets caught at the verifier, and a CA that did record it still publishes the entries to the CRL for simple consumers.
spec/error-codes.md)| Error Code | NPS Status | Description |
|---|---|---|
NIP-CA-GROUP-REVOKED |
NPS-AUTH-FORBIDDEN |
Cannot issue a session under a group that has been revoked. |
NIP-CA-PARENT-NOT-FOUND |
NPS-CLIENT-NOT-FOUND |
The parent_nid / group NID referenced by a session-issue request does not exist. |
NIP-CA-PARENT-NOT-GROUP |
NPS-CLIENT-BAD-PARAM |
The referenced parent NID exists but is not registered as lineage.role = "group". |
NIP-CA-SESSION-VALIDITY-INVALID |
NPS-CLIENT-BAD-PARAM |
Requested session validity is below 60 seconds or above the CA’s configured maximum. |
NIP-CA-JWS-INVALID |
NPS-AUTH-UNAUTHENTICATED |
Group-JWS authorization on a session-issue request fails signature, header, or shape validation. |
NIP-CA-JWS-EXPIRED |
NPS-AUTH-UNAUTHENTICATED |
Group-JWS iat outside the CA’s clock-skew window. |
NIP-CERT-PARENT-REVOKED |
NPS-AUTH-UNAUTHENTICATED |
A session NID’s parent / group NID is revoked or expired (chain check, §3.6). |
| SDK | Phase 1 (this CR) | Notes |
|---|---|---|
.NET (NPS.NIP) |
Required | Reference impl: extends NipCertRecord / INipCaStore / NipCaService, adds NipCaService.RegisterGroupAsync and IssueSessionAsync, adds JWS verification helper, extends HTTP router, adds DB migration 002_orchestrator_session.sql. |
| Python / TypeScript / Java / Rust / Go | Deferred | Tracked as follow-up tickets per the same parity model used for NPS-RFC-0002 / RFC-0003 / RFC-0004. SDK clients can already consume session IdentFrames via the existing IdentFrame DTO and the unknown-field pass-through behavior; what’s deferred is the IssueSessionAsync client-side helper. |
This CR does not add new conformance test items to spec/services/conformance/NPS-Node-L1.md — the L1 suite is Node-side and the group/session model is CA-server-side. Nodes verifying CR-0003 IdentFrames MUST do step 3a (chain check) per §7; this is added to the Node L1 suite as a follow-up under the next test-suite revision.
CA-side correctness is enforced by the unit test suite added to impl/dotnet/tests/NPS.Tests/Nip/:
OrchestratorGroupSessionTests.IssuingGroup_ReturnsLineageGroupFrameOrchestratorGroupSessionTests.IssuingSession_ChainsToGroupOrchestratorGroupSessionTests.IssuingSession_WithoutAuthority_IsRejectedOrchestratorGroupSessionTests.LineageMetadata_IsSigned_AndVerifiesOrchestratorGroupSessionTests.RevokingGroup_BlocksFutureSessions_AndCascades| Audience | Impact |
|---|---|
| Existing single-NID Operators | None. register / renew / revoke paths unchanged; lineage is opt-in. |
| Existing Verifiers (Nodes) | Must add chain-check (§3.6) to remain conformant when receiving session IdentFrames. Frames without lineage continue to verify unchanged. Until upgraded, a Node will fail to verify session IdentFrames (signature mismatch on unknown field) — this is a safe-fail (deny rather than admit). |
| CA Server operators | Must apply DB migration 002_orchestrator_session.sql before upgrading the binary. |
parent_nid / group_nid split anticipates this but the CA enforces a 1-level chain in this CR; deeper chains are a follow-up.subject_nid = group_nid to fan-in by orchestrator; no new entry shape is added here.agent-01-style challenge for session issuance. Sessions are CA-internal; ACME is for external-domain proof and is overkill here. The group-JWS path serves the same anti-replay role.spec/NPS-3-NIP.md updated (§3, §5.1 lineage table, §5.3 reason enum, §7 step 3a, §8 endpoints, §9 errors, §11 changelog v0.7).spec/error-codes.md and spec/NPS-3-NIP.cn.md mirrors updated.spec/cr/README.md index updated.tools/nip-ca-server/db/002_orchestrator_session.sql.NipCertRecord carries NidRole / ParentNid / LineageJson.INipCaStore.GetByParentNidAsync implemented in InMemory / SQLite / PostgreSQL stores.NipCaService.RegisterGroupAsync / IssueSessionAsync / cascading RevokeAsync / chain-checking VerifyAsync.register-group, sessions/issue, groups/{nid}/revoke, groups/{nid}/sessions.- **NPS-CR-0003**: Orchestrator group NIDs and short-lived session NIDs.
Adds `IdentFrame.lineage` (signed), reserves `group-` / `session-`
identifier prefixes, adds the `parent_revoked` revocation reason and
seven new error codes, and ships the CA endpoints
`/v1/orchestrators/groups/{register, sessions/issue, revoke, list}`.
Backward-compatible for ordinary single-NID registration; opt-in for
orchestrators. Reference impl: .NET (`NPS.NIP`); other SDKs deferred.
lineage.session_id duplicate the URN identifier, or be a freer field? Current draft requires it match the URN’s session-... segment for indexability without URN parsing.incident = "cert-revoked"? Tentatively yes but deferred to RFC-0004 follow-up.sessions/issue? Not required by this CR; deployments handle it. RFC notes the threat (a stolen group key issuing thousands of sessions before detection).