Compare commits

...

3 Commits

3 changed files with 75 additions and 10 deletions

View File

@@ -0,0 +1,56 @@
# n_OS_tr Agent Guide: Handling n_signer v0.0.16 Changes
This is a short operational guide for the n_OS_tr agent after the approval-model update in `n_signer`.
## What changed
- `n_signer` is now **deny-by-default**.
- First-time caller/index combinations require approval unless preapproved.
- `nostr_index` values can be auto-derived on approval.
- Policy decisions are visible in activity labels: `:preapprove`, `:session-grant`, `:prompt`, `:no-match`.
## What the agent should do
1. **Always send explicit selector options** in the last `params` item:
- Prefer `{"nostr_index": N}` for service identities.
2. **Expect `policy_denied` on first contact** if caller/index is not preapproved.
3. **Do not retry blindly** on `policy_denied`.
- Surface a clear status: "approval needed".
4. **Treat `unknown_role` as actionable**:
- If using `nostr_index`, request again only after human approval or valid preapprove config.
5. **Use stable caller identity** (service name / VM identity) so preapprove rules match reliably.
## Required startup pattern for n_OS_tr services
Launch `n_signer` with explicit `--preapprove` entries for each boot service that must sign without prompts.
Example:
```ini
ExecStart=/usr/bin/n_signer \
--preapprove caller=qubes:nostr-relay,role=main \
--preapprove caller=qubes:dm-handler,nostr_index=1 \
--preapprove caller=qubes:contacts,nostr_index=2 \
--preapprove caller=qubes:zaps,nostr_index=3
```
## Agent error-handling policy (recommended)
- `policy_denied` → stop, mark as "awaiting approval/preapprove".
- `unknown_role` with `nostr_index` → stop, mark as "identity not yet approved/created".
- `purpose_mismatch` / `curve_mismatch` → configuration bug; fail hard and alert.
- `ambiguous_role_selector` → client bug; send exactly one selector field.
## Observability checks
When troubleshooting, inspect n_signer activity lines:
- `ALLOWED:preapprove` → expected for boot services.
- `ALLOWED:session-grant` → interactive approval happened earlier this session.
- `ALLOWED:prompt` → just approved now.
- `DENIED:no-match` → no policy entry matched caller/index.
## Operational rule of thumb
For n_OS_tr system services, rely on `--preapprove` at startup.
For ad-hoc/manual clients, rely on interactive prompt approvals.

View File

@@ -468,8 +468,8 @@ int socket_name_random(char *out, size_t out_len);
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 16
#define NSIGNER_VERSION "v0.0.16"
#define NSIGNER_VERSION_PATCH 19
#define NSIGNER_VERSION "v0.0.19"
/* NSIGNER_HEADERLESS_DECLS_END */

View File

@@ -271,7 +271,8 @@ typedef struct {
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_ALLOW_SESSION 3 /* allow + save as session grant */
#define POLICY_ALLOW_SESSION_VERB 3 /* allow + save session grant for this caller+role+verb */
#define POLICY_ALLOW_SESSION_ALL 4 /* allow + save session grant for this caller+role (all verbs) */
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
@@ -691,7 +692,8 @@ static int prompt_for_policy_decision(const caller_identity_t *caller,
if (pending_derivation) {
printf(" ** NEW IDENTITY — will be derived if approved **\n");
}
printf("[y] allow once [n] deny [a] allow this caller+role for session\n> ");
printf("[y] allow once [n] deny [e] allow this caller+role+verb for session\n");
printf("[a] allow this caller+role for session (all verbs)\n> ");
fflush(stdout);
ch = getchar();
@@ -704,7 +706,10 @@ static int prompt_for_policy_decision(const caller_identity_t *caller,
ch = tolower(ch);
if (ch == 'a') {
return POLICY_ALLOW_SESSION;
return POLICY_ALLOW_SESSION_ALL;
}
if (ch == 'e') {
return POLICY_ALLOW_SESSION_VERB;
}
if (ch == 'y') {
return POLICY_ALLOW;
@@ -1284,13 +1289,17 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
if (pchk == POLICY_PROMPT) {
pchk = prompt_for_policy_decision(&caller, method, role_name, purpose, pending_derivation);
if (pchk == POLICY_ALLOW_SESSION) {
if (pchk == POLICY_ALLOW_SESSION_VERB || pchk == POLICY_ALLOW_SESSION_ALL) {
policy_entry_t grant;
memset(&grant, 0, sizeof(grant));
strncpy(grant.caller, caller.caller_id, sizeof(grant.caller) - 1);
strncpy(grant.roles[0], role_name, ROLE_NAME_MAX - 1);
grant.role_count = 1;
if (pchk == POLICY_ALLOW_SESSION_VERB) {
strncpy(grant.verbs[0], method, POLICY_VERB_MAX_LEN - 1);
grant.verb_count = 1;
}
grant.prompt = PROMPT_NEVER;
grant.source = POLICY_SOURCE_SESSION_GRANT;
if (policy_table_insert_before_last(ctx->policy, &grant) != 0) {