@ouro.bot/cli 0.1.0-alpha.466 → 0.1.0-alpha.467

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.
package/changelog.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.467",
6
+ "changes": [
7
+ "`ouro dns certificate` can now retrieve the TLS bundle named by a DNS workflow binding and store it as an ordinary workflow-managed vault item without printing private key material.",
8
+ "DNS workflow bindings now reject unknown certificate sources instead of silently treating typos as Porkbun SSL, while preserving the future `acme-dns-01` shape as an explicit not-yet-implemented source.",
9
+ "Porkbun record verification now ignores the provider's default priority zero on non-MX records while still treating real MX priority drift as a planned update."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.466",
6
14
  "changes": [
@@ -329,6 +329,7 @@ function agentResolutionFailureMode(command) {
329
329
  case "vault.item.set":
330
330
  case "vault.item.status":
331
331
  case "vault.item.list":
332
+ case "dns.workflow":
332
333
  case "connect":
333
334
  case "account.ensure":
334
335
  case "mail.import-mbox":
@@ -2177,6 +2178,126 @@ async function executeVaultItemList(command, deps) {
2177
2178
  deps.writeStdout(message);
2178
2179
  return message;
2179
2180
  }
2181
+ function resolveWorkflowFilePath(filePath, deps) {
2182
+ return path.isAbsolute(filePath)
2183
+ ? filePath
2184
+ : path.resolve(deps.getRepoCwd?.() ?? process.cwd(), filePath);
2185
+ }
2186
+ function extractDnsBackupRecords(input) {
2187
+ const value = input;
2188
+ const records = value.plan?.backup?.records ?? value.backup?.records ?? value.records;
2189
+ if (!Array.isArray(records))
2190
+ throw new Error("dns rollback backup does not contain records");
2191
+ return records;
2192
+ }
2193
+ async function executeDnsWorkflow(command, deps) {
2194
+ const workflow = await Promise.resolve().then(() => __importStar(require("./dns-workflow")));
2195
+ const bindingPath = resolveWorkflowFilePath(command.bindingPath, deps);
2196
+ const binding = workflow.loadDnsWorkflowBinding(JSON.parse(fs.readFileSync(bindingPath, "utf-8")));
2197
+ const store = (0, credential_access_1.getCredentialStore)(command.agent);
2198
+ const reader = {
2199
+ readSecretField: async (item, field) => {
2200
+ const raw = await store.getRawSecret(item, "password");
2201
+ const payload = JSON.parse(raw);
2202
+ const value = [payload.secretFields?.[field], payload[field]]
2203
+ .find((candidate) => typeof candidate === "string" && candidate.trim() !== "");
2204
+ if (!value) {
2205
+ throw new Error(`vault item ${item} is missing required secret field ${field}`);
2206
+ }
2207
+ return value;
2208
+ },
2209
+ };
2210
+ const secrets = await workflow.resolveDnsWorkflowSecrets(binding, reader);
2211
+ const driver = workflow.createPorkbunDnsDriver({ fetchImpl: deps.fetchImpl ?? fetch });
2212
+ if (command.action === "certificate") {
2213
+ if (!binding.certificate)
2214
+ throw new Error("DNS workflow binding does not define a certificate");
2215
+ if (binding.certificate.source !== "porkbun-ssl") {
2216
+ throw new Error(`DNS workflow certificate source ${binding.certificate.source} is not implemented`);
2217
+ }
2218
+ const certificate = await driver.retrieveCertificate({ domain: binding.domain, secrets });
2219
+ const payload = {
2220
+ schemaVersion: 1,
2221
+ updatedAt: providerCliNow(deps).toISOString(),
2222
+ publicFields: {
2223
+ domain: binding.domain,
2224
+ host: binding.certificate.host,
2225
+ source: binding.certificate.source,
2226
+ },
2227
+ secretFields: certificate,
2228
+ };
2229
+ await store.store(binding.certificate.storeItem, {
2230
+ username: binding.certificate.host,
2231
+ password: JSON.stringify(payload),
2232
+ notes: "TLS certificate bundle retrieved by DNS workflow. Notes are for human/agent orientation only; workflow bindings and deploy config remain the machine contract.",
2233
+ });
2234
+ const artifactPayload = workflow.redactDnsWorkflowArtifact({
2235
+ action: command.action,
2236
+ binding,
2237
+ certificate: {
2238
+ host: binding.certificate.host,
2239
+ storeItem: binding.certificate.storeItem,
2240
+ ...certificate,
2241
+ },
2242
+ });
2243
+ if (command.outputPath) {
2244
+ const outputPath = resolveWorkflowFilePath(command.outputPath, deps);
2245
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
2246
+ fs.writeFileSync(outputPath, `${JSON.stringify(artifactPayload, null, 2)}\n`, "utf-8");
2247
+ }
2248
+ const message = [
2249
+ `dns certificate for ${binding.domain}`,
2250
+ `driver: ${binding.driver}`,
2251
+ `credential item: vault:${command.agent}:${binding.credentialItem}`,
2252
+ `certificate item: vault:${command.agent}:${binding.certificate.storeItem}`,
2253
+ command.outputPath ? `artifact: ${command.outputPath}` : "artifact: not written",
2254
+ "secret values were not printed",
2255
+ ].join("\n");
2256
+ deps.writeStdout(message);
2257
+ return message;
2258
+ }
2259
+ const currentRecords = await driver.retrieveRecords({ domain: binding.domain, secrets });
2260
+ let plan;
2261
+ if (command.action === "rollback") {
2262
+ const backupPath = command.backupPath;
2263
+ plan = workflow.planDnsRollback({
2264
+ binding,
2265
+ currentRecords,
2266
+ backupRecords: extractDnsBackupRecords(JSON.parse(fs.readFileSync(resolveWorkflowFilePath(backupPath, deps), "utf-8"))),
2267
+ });
2268
+ }
2269
+ else {
2270
+ plan = workflow.planDnsWorkflow({ binding, currentRecords });
2271
+ }
2272
+ const applied = command.action === "apply" || command.action === "rollback"
2273
+ ? await workflow.applyDnsWorkflowPlan({ driver, domain: binding.domain, secrets, plan })
2274
+ : [];
2275
+ const payload = workflow.redactDnsWorkflowArtifact({
2276
+ action: command.action,
2277
+ binding,
2278
+ plan,
2279
+ applied,
2280
+ });
2281
+ if (command.outputPath) {
2282
+ const outputPath = resolveWorkflowFilePath(command.outputPath, deps);
2283
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
2284
+ fs.writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
2285
+ }
2286
+ const changed = plan.changes.length;
2287
+ const preserved = plan.preservedRecords.length;
2288
+ const message = [
2289
+ `dns ${command.action} for ${binding.domain}`,
2290
+ `driver: ${binding.driver}`,
2291
+ `credential item: vault:${command.agent}:${binding.credentialItem}`,
2292
+ `changes: ${changed}`,
2293
+ ...(applied.length > 0 ? [`applied: ${applied.length}`] : []),
2294
+ `preserved records: ${preserved}`,
2295
+ command.outputPath ? `artifact: ${command.outputPath}` : "artifact: not written",
2296
+ "secret values were not printed",
2297
+ ].join("\n");
2298
+ deps.writeStdout(message);
2299
+ return message;
2300
+ }
2180
2301
  function requirePromptSecret(deps, purpose) {
2181
2302
  if (deps.promptSecret)
2182
2303
  return deps.promptSecret;
@@ -2894,6 +3015,53 @@ function mailroomPrivateKeys(mailroom) {
2894
3015
  const keys = isPlainRecord(mailroom?.privateKeys) ? mailroom.privateKeys : {};
2895
3016
  return Object.fromEntries(Object.entries(keys).filter((entry) => typeof entry[1] === "string"));
2896
3017
  }
3018
+ function hostedMailControlConfig(config) {
3019
+ const workSubstrate = isPlainRecord(config.workSubstrate) ? config.workSubstrate : undefined;
3020
+ if (!workSubstrate || stringField(workSubstrate, "mode") !== "hosted")
3021
+ return null;
3022
+ const mailControl = isPlainRecord(workSubstrate.mailControl) ? workSubstrate.mailControl : undefined;
3023
+ const url = mailControl ? stringField(mailControl, "url").replace(/\/+$/, "") : "";
3024
+ const token = mailControl ? (stringField(mailControl, "token") || stringField(mailControl, "adminToken")) : "";
3025
+ if (!url || !token) {
3026
+ throw new Error("hosted workSubstrate.mailControl requires url and token in the agent vault runtime/config item");
3027
+ }
3028
+ return { url, token };
3029
+ }
3030
+ function cleanHostedMailroomBase(existingMailroom) {
3031
+ const base = { ...(existingMailroom ?? {}) };
3032
+ delete base.registryPath;
3033
+ delete base.storePath;
3034
+ return base;
3035
+ }
3036
+ function requiredResponseRecord(value, label) {
3037
+ if (!isPlainRecord(value))
3038
+ throw new Error(`Mail Control response missing ${label}`);
3039
+ return value;
3040
+ }
3041
+ function requiredResponseText(record, key, label) {
3042
+ const value = stringField(record, key);
3043
+ if (!value)
3044
+ throw new Error(`Mail Control response missing ${label}`);
3045
+ return value;
3046
+ }
3047
+ function responsePrivateKeys(value) {
3048
+ if (!isPlainRecord(value))
3049
+ return {};
3050
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[0] === "string" && typeof entry[1] === "string" && entry[1].trim().length > 0));
3051
+ }
3052
+ function requiredHostedKeyIds(body) {
3053
+ return [body.mailbox, body.sourceGrant]
3054
+ .filter(isPlainRecord)
3055
+ .map((record) => stringField(record, "keyId"))
3056
+ .filter((keyId) => keyId.length > 0);
3057
+ }
3058
+ function assertHostedPrivateKeys(input) {
3059
+ for (const keyId of input.requiredKeyIds) {
3060
+ if (input.keys[keyId])
3061
+ continue;
3062
+ throw new Error(`hosted Mail Control references private mail key ${keyId}, but it was not returned and is not present in ${input.agent}'s vault runtime/config. Repair requires a fresh Mail Control one-time key response or key rotation.`);
3063
+ }
3064
+ }
2897
3065
  async function promptDelegatedMailSource(deps, input = {}) {
2898
3066
  if (input.noDelegatedSource)
2899
3067
  return { ownerEmail: "", source: "" };
@@ -2921,8 +3089,11 @@ async function ensureAgentMailroom(agent, input, deps, progressLabel) {
2921
3089
  let stored;
2922
3090
  let mailboxAddress = `${agent.toLowerCase()}@ouro.bot`;
2923
3091
  let sourceAlias = null;
3092
+ let mode = "local";
2924
3093
  let registryPath = path.join(mailStateDir, "registry.json");
2925
3094
  let storePath = mailStateDir;
3095
+ let hostedControlUrl;
3096
+ let blobStoreLabel;
2926
3097
  try {
2927
3098
  progress.startPhase("checking Mailroom runtime config");
2928
3099
  const current = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agent, { preserveCachedOnFailure: true });
@@ -2931,31 +3102,94 @@ async function ensureAgentMailroom(agent, input, deps, progressLabel) {
2931
3102
  }
2932
3103
  const currentConfig = current.ok ? current.config : {};
2933
3104
  const existingMailroom = isPlainRecord(currentConfig.mailroom) ? currentConfig.mailroom : undefined;
3105
+ const hostedConfig = hostedMailControlConfig(currentConfig);
2934
3106
  registryPath = stringField(existingMailroom ?? {}, "registryPath") || registryPath;
2935
3107
  storePath = stringField(existingMailroom ?? {}, "storePath") || storePath;
2936
- progress.updateDetail("ensuring Mailroom identity");
2937
- const ensured = (0, core_1.ensureMailboxRegistry)({
2938
- agentId: agent,
2939
- registry: readMailroomRegistryFromDisk(registryPath),
2940
- keys: mailroomPrivateKeys(existingMailroom),
2941
- ownerEmail: input.ownerEmail || undefined,
2942
- source: input.source || undefined,
2943
- });
2944
- mailboxAddress = ensured.mailboxAddress;
2945
- sourceAlias = ensured.sourceAlias;
2946
- fs.mkdirSync(path.dirname(registryPath), { recursive: true });
2947
- fs.mkdirSync(storePath, { recursive: true });
2948
- fs.writeFileSync(registryPath, `${JSON.stringify(ensured.registry, null, 2)}\n`, "utf-8");
2949
- const nextConfig = {
2950
- ...currentConfig,
2951
- mailroom: {
2952
- ...(existingMailroom ?? {}),
2953
- mailboxAddress,
2954
- registryPath,
2955
- storePath,
2956
- privateKeys: ensured.keys,
2957
- },
2958
- };
3108
+ let nextConfig;
3109
+ if (hostedConfig) {
3110
+ progress.updateDetail("calling hosted Mail Control");
3111
+ const fetchImpl = deps.fetchImpl ?? fetch;
3112
+ const response = await fetchImpl(`${hostedConfig.url}/v1/mailboxes/ensure`, {
3113
+ method: "POST",
3114
+ headers: {
3115
+ authorization: `Bearer ${hostedConfig.token}`,
3116
+ "content-type": "application/json",
3117
+ },
3118
+ body: JSON.stringify({
3119
+ agentId: agent,
3120
+ ...(input.ownerEmail ? { ownerEmail: input.ownerEmail } : {}),
3121
+ ...(input.source ? { source: input.source } : {}),
3122
+ }),
3123
+ });
3124
+ const body = await response.json();
3125
+ if (!response.ok || body.ok === false) {
3126
+ throw new Error(`hosted Mail Control ensure failed (${response.status}): ${body.error ?? response.statusText}`);
3127
+ }
3128
+ const publicRegistry = requiredResponseRecord(body.publicRegistry, "publicRegistry");
3129
+ const blobStore = requiredResponseRecord(body.blobStore, "blobStore");
3130
+ mailboxAddress = typeof body.mailboxAddress === "string" && body.mailboxAddress.trim()
3131
+ ? body.mailboxAddress.trim()
3132
+ : requiredResponseText(requiredResponseRecord(body.mailbox, "mailbox"), "canonicalAddress", "mailboxAddress");
3133
+ sourceAlias = typeof body.sourceAlias === "string" && body.sourceAlias.trim() ? body.sourceAlias.trim() : null;
3134
+ const generatedPrivateKeys = responsePrivateKeys(body.generatedPrivateKeys);
3135
+ const privateKeys = {
3136
+ ...mailroomPrivateKeys(existingMailroom),
3137
+ ...generatedPrivateKeys,
3138
+ };
3139
+ assertHostedPrivateKeys({
3140
+ agent,
3141
+ keys: privateKeys,
3142
+ requiredKeyIds: requiredHostedKeyIds(body),
3143
+ });
3144
+ mode = "hosted";
3145
+ registryPath = null;
3146
+ storePath = null;
3147
+ hostedControlUrl = hostedConfig.url;
3148
+ blobStoreLabel = `${requiredResponseText(blobStore, "azureAccountUrl", "blobStore.azureAccountUrl")}/${requiredResponseText(blobStore, "container", "blobStore.container")}`;
3149
+ nextConfig = {
3150
+ ...currentConfig,
3151
+ mailroom: {
3152
+ ...cleanHostedMailroomBase(existingMailroom),
3153
+ mode,
3154
+ mailboxAddress,
3155
+ sourceAlias,
3156
+ azureAccountUrl: requiredResponseText(blobStore, "azureAccountUrl", "blobStore.azureAccountUrl"),
3157
+ azureContainer: requiredResponseText(blobStore, "container", "blobStore.container"),
3158
+ registryAzureAccountUrl: requiredResponseText(publicRegistry, "azureAccountUrl", "publicRegistry.azureAccountUrl"),
3159
+ registryContainer: requiredResponseText(publicRegistry, "container", "publicRegistry.container"),
3160
+ registryBlob: requiredResponseText(publicRegistry, "blob", "publicRegistry.blob"),
3161
+ registryDomain: requiredResponseText(publicRegistry, "domain", "publicRegistry.domain"),
3162
+ registryRevision: requiredResponseText(publicRegistry, "revision", "publicRegistry.revision"),
3163
+ privateKeys,
3164
+ },
3165
+ };
3166
+ }
3167
+ else {
3168
+ progress.updateDetail("ensuring local Mailroom identity");
3169
+ const ensured = (0, core_1.ensureMailboxRegistry)({
3170
+ agentId: agent,
3171
+ registry: readMailroomRegistryFromDisk(registryPath),
3172
+ keys: mailroomPrivateKeys(existingMailroom),
3173
+ ownerEmail: input.ownerEmail || undefined,
3174
+ source: input.source || undefined,
3175
+ });
3176
+ mailboxAddress = ensured.mailboxAddress;
3177
+ sourceAlias = ensured.sourceAlias;
3178
+ fs.mkdirSync(path.dirname(registryPath), { recursive: true });
3179
+ fs.mkdirSync(storePath, { recursive: true });
3180
+ fs.writeFileSync(registryPath, `${JSON.stringify(ensured.registry, null, 2)}\n`, "utf-8");
3181
+ nextConfig = {
3182
+ ...currentConfig,
3183
+ mailroom: {
3184
+ ...(existingMailroom ?? {}),
3185
+ mode,
3186
+ mailboxAddress,
3187
+ registryPath,
3188
+ storePath,
3189
+ privateKeys: ensured.keys,
3190
+ },
3191
+ };
3192
+ }
2959
3193
  progress.updateDetail("storing private mail keys in vault");
2960
3194
  stored = await (0, runtime_credentials_1.upsertRuntimeCredentialConfig)(agent, nextConfig, providerCliNow(deps));
2961
3195
  progress.updateDetail("enabling Mail sense in agent.json");
@@ -2971,7 +3205,7 @@ async function ensureAgentMailroom(agent, input, deps, progressLabel) {
2971
3205
  if (!stored)
2972
3206
  throw new Error("Mailroom setup did not store runtime credentials");
2973
3207
  const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
2974
- return { mailboxAddress, sourceAlias, registryPath, storePath, stored, syncSummary };
3208
+ return { mailboxAddress, sourceAlias, mode, registryPath, storePath, hostedControlUrl, blobStoreLabel, stored, syncSummary };
2975
3209
  }
2976
3210
  async function executeConnectMail(agent, deps, input = {}) {
2977
3211
  writeConnectorIntro(deps, {
@@ -3016,8 +3250,10 @@ async function executeConnectMail(agent, deps, input = {}) {
3016
3250
  whatChanged: [
3017
3251
  `Mailbox: ${outcome.mailboxAddress}`,
3018
3252
  ...(outcome.sourceAlias ? [`Delegated alias: ${outcome.sourceAlias}`] : []),
3019
- `Registry: ${outcome.registryPath}`,
3020
- `Encrypted store: ${outcome.storePath}`,
3253
+ ...(outcome.hostedControlUrl ? [`Hosted Mail Control: ${outcome.hostedControlUrl}`] : []),
3254
+ ...(outcome.blobStoreLabel ? [`Blob store: ${outcome.blobStoreLabel}`] : []),
3255
+ ...(outcome.registryPath ? [`Registry: ${outcome.registryPath}`] : []),
3256
+ ...(outcome.storePath ? [`Encrypted store: ${outcome.storePath}`] : []),
3021
3257
  `Stored: ${outcome.stored.itemPath}`,
3022
3258
  "agent.json: senses.mail.enabled = true",
3023
3259
  "private mail keys were not printed",
@@ -3033,8 +3269,10 @@ async function executeConnectMail(agent, deps, input = {}) {
3033
3269
  `Agent Mail connected for ${agent}`,
3034
3270
  `mailbox: ${outcome.mailboxAddress}`,
3035
3271
  ...(outcome.sourceAlias ? [`delegated alias: ${outcome.sourceAlias}`] : []),
3036
- `registry: ${outcome.registryPath}`,
3037
- `encrypted store: ${outcome.storePath}`,
3272
+ ...(outcome.hostedControlUrl ? [`Hosted Mail Control: ${outcome.hostedControlUrl}`] : []),
3273
+ ...(outcome.blobStoreLabel ? [`Blob store: ${outcome.blobStoreLabel}`] : []),
3274
+ ...(outcome.registryPath ? [`registry: ${outcome.registryPath}`] : []),
3275
+ ...(outcome.storePath ? [`encrypted store: ${outcome.storePath}`] : []),
3038
3276
  `stored: ${outcome.stored.itemPath}`,
3039
3277
  "agent.json: senses.mail.enabled = true",
3040
3278
  "private mail keys were not printed",
@@ -3079,9 +3317,12 @@ async function executeAccountEnsure(command, deps) {
3079
3317
  "Vault item: runtime/config",
3080
3318
  `Mailbox: ${outcome.mailboxAddress}`,
3081
3319
  ...(outcome.sourceAlias ? [`Delegated alias: ${outcome.sourceAlias}`] : []),
3082
- `Registry: ${outcome.registryPath}`,
3083
- `Encrypted store: ${outcome.storePath}`,
3320
+ ...(outcome.hostedControlUrl ? [`Hosted Mail Control: ${outcome.hostedControlUrl}`] : []),
3321
+ ...(outcome.blobStoreLabel ? [`Blob store: ${outcome.blobStoreLabel}`] : []),
3322
+ ...(outcome.registryPath ? [`Registry: ${outcome.registryPath}`] : []),
3323
+ ...(outcome.storePath ? [`Encrypted store: ${outcome.storePath}`] : []),
3084
3324
  "agent.json: senses.mail.enabled = true",
3325
+ "private mail keys were not printed",
3085
3326
  ...(outcome.syncSummary ? [outcome.syncSummary] : []),
3086
3327
  ],
3087
3328
  nextMoves: [
@@ -3094,9 +3335,12 @@ async function executeAccountEnsure(command, deps) {
3094
3335
  "vault: runtime/config",
3095
3336
  `mailbox: ${outcome.mailboxAddress}`,
3096
3337
  ...(outcome.sourceAlias ? [`delegated alias: ${outcome.sourceAlias}`] : []),
3097
- `registry: ${outcome.registryPath}`,
3098
- `encrypted store: ${outcome.storePath}`,
3338
+ ...(outcome.hostedControlUrl ? [`Hosted Mail Control: ${outcome.hostedControlUrl}`] : []),
3339
+ ...(outcome.blobStoreLabel ? [`Blob store: ${outcome.blobStoreLabel}`] : []),
3340
+ ...(outcome.registryPath ? [`registry: ${outcome.registryPath}`] : []),
3341
+ ...(outcome.storePath ? [`encrypted store: ${outcome.storePath}`] : []),
3099
3342
  "agent.json: senses.mail.enabled = true",
3343
+ "private mail keys were not printed",
3100
3344
  ...(outcome.syncSummary ? [outcome.syncSummary] : []),
3101
3345
  ],
3102
3346
  });
@@ -5531,6 +5775,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
5531
5775
  if (command.kind === "vault.item.list") {
5532
5776
  return executeVaultItemList(command, deps);
5533
5777
  }
5778
+ if (command.kind === "dns.workflow") {
5779
+ return executeDnsWorkflow(command, deps);
5780
+ }
5534
5781
  // ── auth (local, no daemon socket needed) ──
5535
5782
  if (command.kind === "auth.run") {
5536
5783
  return executeAuthRun(command, deps);
@@ -101,6 +101,7 @@ function usage() {
101
101
  " ouro vault item list [--agent <name>] [--prefix <path-prefix>]",
102
102
  " ouro vault ops porkbun set [--agent <name>] --account <account>",
103
103
  " ouro vault ops porkbun status [--agent <name>] [--account <account>]",
104
+ " ouro dns backup|plan|apply|verify|rollback|certificate [--agent <name>] --binding <path> [--output <path>] [--backup <path>] [--yes]",
104
105
  " ouro chat <agent>",
105
106
  " ouro msg --to <agent> [--session <id>] [--task <ref>] <message>",
106
107
  " ouro poke <agent> --task <task-id>",
@@ -681,6 +682,74 @@ function parseVaultOpsCommand(args) {
681
682
  compatibilityAlias: vault_items_1.PORKBUN_OPS_COMPATIBILITY_ALIAS,
682
683
  };
683
684
  }
685
+ function isDnsWorkflowAction(value) {
686
+ return value === "backup" || value === "plan" || value === "apply" || value === "verify" || value === "rollback" || value === "certificate";
687
+ }
688
+ function normalizeWorkflowPath(value, label) {
689
+ const trimmed = value?.trim() ?? "";
690
+ if (!trimmed || /[\r\n\t]/.test(trimmed)) {
691
+ throw new Error(`${label} must be a non-empty path without control characters.`);
692
+ }
693
+ return trimmed;
694
+ }
695
+ function parseDnsCommand(args) {
696
+ const action = args[0];
697
+ if (!isDnsWorkflowAction(action)) {
698
+ throw new Error("Usage: ouro dns backup|plan|apply|verify|rollback|certificate [--agent <name>] --binding <path>");
699
+ }
700
+ const { agent, rest } = extractAgentFlag(args.slice(1));
701
+ let bindingPath;
702
+ let outputPath;
703
+ let backupPath;
704
+ let yes = false;
705
+ for (let i = 0; i < rest.length; i += 1) {
706
+ const token = rest[i];
707
+ if (token === "--binding") {
708
+ bindingPath = normalizeWorkflowPath(rest[i + 1], "dns --binding");
709
+ i += 1;
710
+ continue;
711
+ }
712
+ if (token === "--output") {
713
+ outputPath = normalizeWorkflowPath(rest[i + 1], "dns --output");
714
+ i += 1;
715
+ continue;
716
+ }
717
+ if (token === "--backup") {
718
+ backupPath = normalizeWorkflowPath(rest[i + 1], "dns --backup");
719
+ i += 1;
720
+ continue;
721
+ }
722
+ if (token === "--yes") {
723
+ yes = true;
724
+ continue;
725
+ }
726
+ if (token === "--credential-item") {
727
+ throw new Error("credential item belongs in the DNS workflow binding");
728
+ }
729
+ throw new Error(`Usage: ouro dns ${action} [--agent <name>] --binding <path>`);
730
+ }
731
+ if (!bindingPath) {
732
+ throw new Error(`Usage: ouro dns ${action} [--agent <name>] --binding <path>`);
733
+ }
734
+ if (action === "apply" && !yes) {
735
+ throw new Error("dns apply requires --yes after a reviewed dry-run");
736
+ }
737
+ if (action === "rollback") {
738
+ if (!backupPath)
739
+ throw new Error("dns rollback requires --backup <path>");
740
+ if (!yes)
741
+ throw new Error("dns rollback requires --yes after choosing a backup");
742
+ }
743
+ return {
744
+ kind: "dns.workflow",
745
+ action,
746
+ ...(agent ? { agent } : {}),
747
+ bindingPath,
748
+ ...(outputPath ? { outputPath } : {}),
749
+ ...(backupPath ? { backupPath } : {}),
750
+ ...(yes ? { yes: true } : {}),
751
+ };
752
+ }
684
753
  function parseVaultConfigCommand(args) {
685
754
  const sub = args[0];
686
755
  const { agent, rest } = extractAgentFlag(args.slice(1));
@@ -1330,6 +1399,8 @@ function parseOuroCommand(args) {
1330
1399
  return parseProviderCommand(args.slice(1));
1331
1400
  if (head === "mail")
1332
1401
  return parseMailCommand(args.slice(1));
1402
+ if (head === "dns")
1403
+ return parseDnsCommand(args.slice(1));
1333
1404
  if (head === "logs") {
1334
1405
  if (second === "prune")
1335
1406
  return { kind: "daemon.logs.prune" };
@@ -0,0 +1,365 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadDnsWorkflowBinding = loadDnsWorkflowBinding;
4
+ exports.resolveDnsWorkflowSecrets = resolveDnsWorkflowSecrets;
5
+ exports.createPorkbunDnsDriver = createPorkbunDnsDriver;
6
+ exports.planDnsWorkflow = planDnsWorkflow;
7
+ exports.planDnsRollback = planDnsRollback;
8
+ exports.applyDnsWorkflowPlan = applyDnsWorkflowPlan;
9
+ exports.redactDnsWorkflowArtifact = redactDnsWorkflowArtifact;
10
+ const runtime_1 = require("../../nerves/runtime");
11
+ function isRecordType(value) {
12
+ return value === "A" || value === "AAAA" || value === "CNAME" || value === "MX" || value === "TXT";
13
+ }
14
+ function requireString(value, label) {
15
+ if (typeof value !== "string" || value.trim() === "")
16
+ throw new Error(`${label} is required`);
17
+ return value.trim();
18
+ }
19
+ function requireRecordType(value, label) {
20
+ if (!isRecordType(value))
21
+ throw new Error(`${label} must be A, AAAA, CNAME, MX, or TXT`);
22
+ return value;
23
+ }
24
+ function parseCertificateSource(value) {
25
+ if (value === undefined || value === "porkbun-ssl")
26
+ return "porkbun-ssl";
27
+ if (value === "acme-dns-01")
28
+ return "acme-dns-01";
29
+ throw new Error("certificate.source must be porkbun-ssl or acme-dns-01");
30
+ }
31
+ function recordKey(record) {
32
+ return `${record.type}:${record.name}`;
33
+ }
34
+ function parseRecord(input, label) {
35
+ const value = input;
36
+ const record = {
37
+ ...(typeof value.id === "string" ? { id: value.id } : {}),
38
+ type: requireRecordType(value.type, `${label}.type`),
39
+ name: requireString(value.name, `${label}.name`),
40
+ content: requireString(value.content, `${label}.content`),
41
+ ...(typeof value.ttl === "number" ? { ttl: value.ttl } : {}),
42
+ ...(typeof value.priority === "number" ? { priority: value.priority } : {}),
43
+ };
44
+ return record;
45
+ }
46
+ function parseResourceRecord(input, label) {
47
+ const value = input;
48
+ return {
49
+ type: requireRecordType(value.type, `${label}.type`),
50
+ name: requireString(value.name, `${label}.name`),
51
+ };
52
+ }
53
+ function assertNoCredentialOntology(input) {
54
+ if ("credentialItemNoteQuery" in input || "noteQuery" in input || "notes" in input) {
55
+ throw new Error("notes are not machine contracts");
56
+ }
57
+ if ("authority" in input || "kind" in input) {
58
+ throw new Error("workflow binding must not give a vault item assumed use");
59
+ }
60
+ }
61
+ function loadDnsWorkflowBinding(input) {
62
+ if (!input || typeof input !== "object")
63
+ throw new Error("DNS workflow binding must be an object");
64
+ const value = input;
65
+ assertNoCredentialOntology(value);
66
+ if (value.workflow !== "dns")
67
+ throw new Error("DNS workflow binding must set workflow to dns");
68
+ if (value.driver !== "porkbun")
69
+ throw new Error("DNS workflow binding driver must be porkbun");
70
+ const resources = value.resources;
71
+ const desired = value.desired;
72
+ if (!Array.isArray(resources?.records) || resources.records.length === 0) {
73
+ throw new Error("DNS workflow binding requires a resource allowlist");
74
+ }
75
+ if (!Array.isArray(desired?.records))
76
+ throw new Error("DNS workflow binding requires desired records");
77
+ const certificate = value.certificate;
78
+ return {
79
+ workflow: "dns",
80
+ domain: requireString(value.domain, "domain"),
81
+ driver: "porkbun",
82
+ credentialItem: requireString(value.credentialItem, "credentialItem"),
83
+ resources: {
84
+ records: resources.records.map((record, index) => parseResourceRecord(record, `resources.records[${index}]`)),
85
+ },
86
+ desired: {
87
+ records: desired.records.map((record, index) => parseRecord(record, `desired.records[${index}]`)),
88
+ },
89
+ ...(certificate ? {
90
+ certificate: {
91
+ host: requireString(certificate.host, "certificate.host"),
92
+ source: parseCertificateSource(certificate.source),
93
+ storeItem: requireString(certificate.storeItem, "certificate.storeItem"),
94
+ ...(certificate.acmeChallengeRecord
95
+ ? {
96
+ acmeChallengeRecord: parseResourceRecord(certificate.acmeChallengeRecord, "certificate.acmeChallengeRecord"),
97
+ }
98
+ : {}),
99
+ },
100
+ } : {}),
101
+ };
102
+ }
103
+ async function resolveDnsWorkflowSecrets(binding, reader) {
104
+ return {
105
+ apiKey: await reader.readSecretField(binding.credentialItem, "apiKey"),
106
+ secretApiKey: await reader.readSecretField(binding.credentialItem, "secretApiKey"),
107
+ };
108
+ }
109
+ async function readPorkbunJson(response) {
110
+ const payload = await response.json();
111
+ if (!response.ok || payload.status === "ERROR") {
112
+ throw new Error(payload.message ?? `Porkbun request failed with status ${response.status}`);
113
+ }
114
+ return payload;
115
+ }
116
+ function porkbunHeaders(secrets) {
117
+ return {
118
+ "X-API-Key": secrets.apiKey,
119
+ "X-Secret-API-Key": secrets.secretApiKey,
120
+ };
121
+ }
122
+ function porkbunRecordBody(record) {
123
+ return {
124
+ type: record.type,
125
+ name: record.name === "@" ? "" : record.name,
126
+ content: record.content,
127
+ ttl: record.ttl ?? 600,
128
+ prio: record.priority ?? 0,
129
+ };
130
+ }
131
+ function normalizePorkbunRecordName(domain, name) {
132
+ const suffix = `.${domain}`;
133
+ if (name === domain)
134
+ return "@";
135
+ if (name.endsWith(suffix))
136
+ return name.slice(0, -suffix.length);
137
+ return name;
138
+ }
139
+ function normalizePorkbunNumber(value) {
140
+ const parsed = value === null || value === undefined || value === "" ? Number.NaN : Number(value);
141
+ return Number.isFinite(parsed) ? parsed : undefined;
142
+ }
143
+ function normalizePorkbunRecord(domain, input) {
144
+ const value = input;
145
+ const ttl = normalizePorkbunNumber(value.ttl);
146
+ const priority = normalizePorkbunNumber(value.priority ?? value.prio);
147
+ return {
148
+ ...(typeof value.id === "string" ? { id: value.id } : {}),
149
+ type: requireString(value.type, "provider record type"),
150
+ name: normalizePorkbunRecordName(domain, requireString(value.name, "provider record name")),
151
+ content: requireString(value.content, "provider record content"),
152
+ ...(ttl === undefined ? {} : { ttl }),
153
+ ...(priority === undefined ? {} : { priority }),
154
+ };
155
+ }
156
+ async function emitPorkbunRequest(input) {
157
+ (0, runtime_1.emitNervesEvent)({
158
+ event: "daemon.dns_provider_request_start",
159
+ component: "daemon",
160
+ message: `DNS provider ${input.method} ${input.path} started`,
161
+ meta: {
162
+ driver: "porkbun",
163
+ method: input.method,
164
+ path: input.path,
165
+ },
166
+ });
167
+ try {
168
+ const result = await input.execute();
169
+ (0, runtime_1.emitNervesEvent)({
170
+ event: "daemon.dns_provider_request_end",
171
+ component: "daemon",
172
+ message: `DNS provider ${input.method} ${input.path} completed`,
173
+ meta: {
174
+ driver: "porkbun",
175
+ method: input.method,
176
+ path: input.path,
177
+ },
178
+ });
179
+ return result;
180
+ }
181
+ catch (error) {
182
+ (0, runtime_1.emitNervesEvent)({
183
+ level: "error",
184
+ event: "daemon.dns_provider_request_error",
185
+ component: "daemon",
186
+ message: `DNS provider ${input.method} ${input.path} failed`,
187
+ meta: {
188
+ driver: "porkbun",
189
+ method: input.method,
190
+ path: input.path,
191
+ error: String(error),
192
+ },
193
+ });
194
+ throw error;
195
+ }
196
+ }
197
+ function createPorkbunDnsDriver(options) {
198
+ const baseUrl = (options.baseUrl ?? "https://api.porkbun.com/api/json/v3").replace(/\/+$/, "");
199
+ const readOnly = async (path, secrets) => {
200
+ return emitPorkbunRequest({
201
+ method: "GET",
202
+ path,
203
+ execute: async () => readPorkbunJson(await options.fetchImpl(`${baseUrl}${path}`, {
204
+ method: "GET",
205
+ headers: porkbunHeaders(secrets),
206
+ })),
207
+ });
208
+ };
209
+ const mutate = async (path, secrets, body = {}) => {
210
+ return emitPorkbunRequest({
211
+ method: "POST",
212
+ path,
213
+ execute: async () => readPorkbunJson(await options.fetchImpl(`${baseUrl}${path}`, {
214
+ method: "POST",
215
+ headers: {
216
+ ...porkbunHeaders(secrets),
217
+ "Content-Type": "application/json",
218
+ },
219
+ body: JSON.stringify(body),
220
+ })),
221
+ });
222
+ };
223
+ return {
224
+ async ping(secrets) {
225
+ const payload = await readOnly("/ping", secrets);
226
+ return { credentialsValid: payload.credentialsValid === true };
227
+ },
228
+ async retrieveRecords({ domain, secrets }) {
229
+ const payload = await readOnly(`/dns/retrieve/${encodeURIComponent(domain)}`, secrets);
230
+ return (payload.records ?? []).map((record) => normalizePorkbunRecord(domain, record));
231
+ },
232
+ async retrieveCertificate({ domain, secrets }) {
233
+ const payload = await readOnly(`/ssl/retrieve/${encodeURIComponent(domain)}`, secrets);
234
+ return {
235
+ certificatechain: requireString(payload.certificatechain, "certificatechain"),
236
+ publickey: requireString(payload.publickey, "publickey"),
237
+ privatekey: requireString(payload.privatekey, "privatekey"),
238
+ };
239
+ },
240
+ async createRecord({ domain, secrets, record }) {
241
+ const payload = await mutate(`/dns/create/${encodeURIComponent(domain)}`, secrets, porkbunRecordBody(record));
242
+ return typeof payload.id === "string" ? { id: payload.id } : {};
243
+ },
244
+ async editRecord({ domain, secrets, id, record }) {
245
+ await mutate(`/dns/edit/${encodeURIComponent(domain)}/${encodeURIComponent(id)}`, secrets, porkbunRecordBody(record));
246
+ },
247
+ async deleteRecord({ domain, secrets, id }) {
248
+ await mutate(`/dns/delete/${encodeURIComponent(domain)}/${encodeURIComponent(id)}`, secrets);
249
+ },
250
+ };
251
+ }
252
+ function assertDesiredRecordsAllowed(binding) {
253
+ const allowed = new Set(binding.resources.records.map(recordKey));
254
+ for (const desired of binding.desired.records) {
255
+ if (!allowed.has(recordKey(desired)))
256
+ throw new Error("desired DNS record is outside DNS workflow allowlist");
257
+ }
258
+ }
259
+ function recordsEqual(left, right) {
260
+ const priorityEqual = left.type === "MX"
261
+ ? (left.priority ?? 0) === (right.priority ?? 0)
262
+ : true;
263
+ return left.type === right.type &&
264
+ left.name === right.name &&
265
+ left.content === right.content &&
266
+ left.ttl === right.ttl &&
267
+ priorityEqual;
268
+ }
269
+ function planDnsWorkflow(input) {
270
+ assertDesiredRecordsAllowed(input.binding);
271
+ const allowedKeys = new Set(input.binding.resources.records.map(recordKey));
272
+ const desiredKeys = new Set(input.binding.desired.records.map(recordKey));
273
+ const changes = [];
274
+ for (const desired of input.binding.desired.records) {
275
+ const current = input.currentRecords.find((record) => recordKey(record) === recordKey(desired));
276
+ if (!current) {
277
+ changes.push({ action: "create", record: desired, reason: "desired record is missing" });
278
+ }
279
+ else if (!recordsEqual(current, desired)) {
280
+ changes.push({ action: "update", record: desired, currentRecord: current, reason: "desired record differs from current provider record" });
281
+ }
282
+ }
283
+ if (input.deleteExtraAllowedRecords) {
284
+ for (const current of input.currentRecords) {
285
+ if (allowedKeys.has(recordKey(current)) && !desiredKeys.has(recordKey(current))) {
286
+ changes.push({ action: "delete", record: current, currentRecord: current, reason: "allowlisted record is absent from rollback backup" });
287
+ }
288
+ }
289
+ }
290
+ const preservedRecords = input.currentRecords.filter((record) => !desiredKeys.has(recordKey(record)));
291
+ return {
292
+ backup: { domain: input.binding.domain, records: input.currentRecords },
293
+ changes,
294
+ preservedRecords,
295
+ certificateActions: input.binding.certificate
296
+ ? [{
297
+ action: "retrieve-and-store",
298
+ host: input.binding.certificate.host,
299
+ secretItem: input.binding.certificate.storeItem,
300
+ }]
301
+ : [],
302
+ };
303
+ }
304
+ function planDnsRollback(input) {
305
+ const allowedKeys = new Set(input.binding.resources.records.map(recordKey));
306
+ const rollbackBinding = {
307
+ ...input.binding,
308
+ desired: {
309
+ records: input.backupRecords.filter((record) => allowedKeys.has(recordKey(record))),
310
+ },
311
+ };
312
+ return planDnsWorkflow({
313
+ binding: rollbackBinding,
314
+ currentRecords: input.currentRecords,
315
+ deleteExtraAllowedRecords: true,
316
+ });
317
+ }
318
+ async function applyDnsWorkflowPlan(input) {
319
+ const applied = [];
320
+ for (const change of input.plan.changes) {
321
+ if (change.action === "create") {
322
+ const result = await input.driver.createRecord({ domain: input.domain, secrets: input.secrets, record: change.record });
323
+ applied.push({ action: "create", record: change.record, ...(result.id ? { id: result.id } : {}) });
324
+ continue;
325
+ }
326
+ const id = change.currentRecord?.id ?? change.record.id;
327
+ if (!id)
328
+ throw new Error(`cannot ${change.action} ${change.record.type} ${change.record.name} without provider record id`);
329
+ if (change.action === "update") {
330
+ await input.driver.editRecord({ domain: input.domain, secrets: input.secrets, id, record: change.record });
331
+ applied.push({ action: "update", record: change.record, id });
332
+ continue;
333
+ }
334
+ await input.driver.deleteRecord({ domain: input.domain, secrets: input.secrets, id });
335
+ applied.push({ action: "delete", record: change.record, id });
336
+ }
337
+ return applied;
338
+ }
339
+ function redactedValueForKey(key, value) {
340
+ const normalized = key.toLowerCase();
341
+ if (normalized === "apikey" || normalized === "secretapikey" || normalized === "x-api-key" || normalized === "x-secret-api-key") {
342
+ return "[redacted]";
343
+ }
344
+ if (normalized === "privatekey" || normalized === "privatekeypem" || normalized === "privatekeypath") {
345
+ return "[redacted]";
346
+ }
347
+ if (typeof value === "string" && value.includes("BEGIN PRIVATE KEY")) {
348
+ return "[redacted]";
349
+ }
350
+ return undefined;
351
+ }
352
+ function redactDnsWorkflowArtifact(input) {
353
+ if (Array.isArray(input))
354
+ return input.map((item) => redactDnsWorkflowArtifact(item));
355
+ if (input && typeof input === "object") {
356
+ const output = {};
357
+ for (const [key, value] of Object.entries(input)) {
358
+ output[key] = redactedValueForKey(key, value) ?? redactDnsWorkflowArtifact(value);
359
+ }
360
+ return output;
361
+ }
362
+ if (typeof input === "string" && input.includes("BEGIN PRIVATE KEY"))
363
+ return "[redacted]";
364
+ return input;
365
+ }
@@ -5,6 +5,7 @@ exports.readMailMessageView = readMailMessageView;
5
5
  const runtime_1 = require("../../../nerves/runtime");
6
6
  const file_store_1 = require("../../../mailroom/file-store");
7
7
  const reader_1 = require("../../../mailroom/reader");
8
+ const core_1 = require("../../../mailroom/core");
8
9
  const OUTLOOK_MAIL_LIST_LIMIT = 50;
9
10
  const OUTLOOK_MAIL_COUNT_LIMIT = 500;
10
11
  const OUTLOOK_MAIL_BODY_LIMIT = 12_000;
@@ -92,13 +93,27 @@ function buildFolders(messages, outbound) {
92
93
  { id: "native", label: "Native", count: messages.filter((message) => message.compartmentKind === "native").length },
93
94
  ];
94
95
  const sourceCounts = new Map();
96
+ const sourceOwnerCounts = new Map();
95
97
  for (const message of messages) {
96
98
  if (!message.source)
97
99
  continue;
98
100
  sourceCounts.set(message.source, (sourceCounts.get(message.source) ?? 0) + 1);
101
+ const owner = message.ownerEmail ?? "";
102
+ const ownerCounts = sourceOwnerCounts.get(message.source) ?? new Map();
103
+ ownerCounts.set(owner, (ownerCounts.get(owner) ?? 0) + 1);
104
+ sourceOwnerCounts.set(message.source, ownerCounts);
99
105
  }
100
106
  for (const [source, count] of [...sourceCounts.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
101
- folders.push({ id: `source:${source}`, label: source.toUpperCase(), count });
107
+ const ownerCounts = sourceOwnerCounts.get(source);
108
+ if (!ownerCounts || ownerCounts.size <= 1) {
109
+ folders.push({ id: `source:${source}`, label: source.toUpperCase(), count });
110
+ continue;
111
+ }
112
+ for (const [owner, ownerCount] of [...ownerCounts.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
113
+ const ownerLabel = owner || "unknown owner";
114
+ const ownerId = owner || "unknown-owner";
115
+ folders.push({ id: `source:${source}:${ownerId}`, label: `${source.toUpperCase()} / ${ownerLabel}`, count: ownerCount });
116
+ }
102
117
  }
103
118
  return folders;
104
119
  }
@@ -123,6 +138,10 @@ function outboundRecord(record) {
123
138
  return {
124
139
  id: record.id,
125
140
  status: record.status,
141
+ mailboxRole: record.mailboxRole ?? "agent-native-mailbox",
142
+ sendAuthority: record.sendAuthority ?? "agent-native",
143
+ ownerEmail: record.ownerEmail ?? null,
144
+ source: record.source ?? null,
126
145
  from: record.from,
127
146
  to: record.to,
128
147
  cc: record.cc,
@@ -152,9 +171,22 @@ function accessEntries(entries) {
152
171
  threadId: entry.threadId ?? null,
153
172
  tool: entry.tool,
154
173
  reason: entry.reason,
174
+ mailboxRole: entry.mailboxRole ?? null,
175
+ compartmentKind: entry.compartmentKind ?? null,
176
+ ownerEmail: entry.ownerEmail ?? null,
177
+ source: entry.source ?? null,
155
178
  accessedAt: entry.accessedAt,
156
179
  }));
157
180
  }
181
+ function accessProvenance(message) {
182
+ const provenance = (0, core_1.describeMailProvenance)(message);
183
+ return {
184
+ mailboxRole: provenance.mailboxRole,
185
+ compartmentKind: message.compartmentKind,
186
+ ownerEmail: provenance.ownerEmail,
187
+ source: provenance.source,
188
+ };
189
+ }
158
190
  function emitMailRead(agentName, mode, status) {
159
191
  (0, runtime_1.emitNervesEvent)({
160
192
  component: "heart",
@@ -238,6 +270,7 @@ async function readMailMessageView(agentName, messageId) {
238
270
  messageId,
239
271
  tool: "outlook_mail_message",
240
272
  reason: "outlook read-only message body",
273
+ ...accessProvenance(decrypted),
241
274
  });
242
275
  const body = decrypted.private.text.length > OUTLOOK_MAIL_BODY_LIMIT
243
276
  ? decrypted.private.text.slice(0, OUTLOOK_MAIL_BODY_LIMIT)
@@ -74,6 +74,14 @@ function displaySender(candidate) {
74
74
  }
75
75
  return candidate.senderEmail;
76
76
  }
77
+ function candidateCompartmentKind(candidate) {
78
+ return candidate.ownerEmail || candidate.source ? "delegated" : "native";
79
+ }
80
+ function candidateMailboxRole(candidate) {
81
+ return candidateCompartmentKind(candidate) === "delegated"
82
+ ? "delegated-human-mailbox"
83
+ : "agent-native-mailbox";
84
+ }
77
85
  function renderAttentionContent(candidate) {
78
86
  return [
79
87
  "[Mail Screener]",
@@ -92,6 +100,7 @@ function renderAttentionContent(candidate) {
92
100
  ].filter(Boolean).join("\n");
93
101
  }
94
102
  function queuedSummary(candidate, queuedAt) {
103
+ const compartmentKind = candidateCompartmentKind(candidate);
95
104
  return {
96
105
  candidateId: candidate.id,
97
106
  messageId: candidate.messageId,
@@ -99,6 +108,10 @@ function queuedSummary(candidate, queuedAt) {
99
108
  senderDisplay: candidate.senderDisplay,
100
109
  recipient: candidate.recipient,
101
110
  placement: candidate.placement,
111
+ mailboxRole: candidateMailboxRole(candidate),
112
+ compartmentKind,
113
+ ownerEmail: candidate.ownerEmail ?? null,
114
+ source: candidate.source ?? null,
102
115
  queuedAt,
103
116
  };
104
117
  }
@@ -42,6 +42,7 @@ exports.decryptMailPayload = decryptMailPayload;
42
42
  exports.encryptJsonForMailKey = encryptJsonForMailKey;
43
43
  exports.decryptMailJson = decryptMailJson;
44
44
  exports.resolveMailAddress = resolveMailAddress;
45
+ exports.describeMailProvenance = describeMailProvenance;
45
46
  exports.buildStoredMailMessage = buildStoredMailMessage;
46
47
  exports.decryptStoredMailMessage = decryptStoredMailMessage;
47
48
  exports.provisionMailboxRegistry = provisionMailboxRegistry;
@@ -237,6 +238,32 @@ function resolveMailAddress(registry, address) {
237
238
  defaultPlacement: grant.defaultPlacement,
238
239
  };
239
240
  }
241
+ function describeMailProvenance(message) {
242
+ if (message.compartmentKind === "delegated") {
243
+ const ownerEmail = message.ownerEmail ?? null;
244
+ const source = message.source ?? null;
245
+ const ownerLabel = ownerEmail ?? "unknown owner";
246
+ const sourceLabel = source ?? "unknown source";
247
+ return {
248
+ mailboxRole: "delegated-human-mailbox",
249
+ mailboxLabel: `${ownerLabel} / ${sourceLabel} delegated to ${message.agentId}`,
250
+ agentId: message.agentId,
251
+ ownerEmail,
252
+ source,
253
+ recipient: message.recipient,
254
+ sendAsHumanAllowed: false,
255
+ };
256
+ }
257
+ return {
258
+ mailboxRole: "agent-native-mailbox",
259
+ mailboxLabel: `${message.recipient} (native agent mail)`,
260
+ agentId: message.agentId,
261
+ ownerEmail: null,
262
+ source: null,
263
+ recipient: message.recipient,
264
+ sendAsHumanAllowed: false,
265
+ };
266
+ }
240
267
  function addressList(values) {
241
268
  /* v8 ignore next -- parsedAddressList filters undefined top-level values; this guards malformed address-group entries. @preserve */
242
269
  return (values ?? [])
@@ -96,6 +96,10 @@ async function createMailDraft(input) {
96
96
  id: draftId(),
97
97
  agentId: input.agentId,
98
98
  status: "draft",
99
+ mailboxRole: "agent-native-mailbox",
100
+ sendAuthority: "agent-native",
101
+ ownerEmail: null,
102
+ source: null,
99
103
  from: (0, core_1.normalizeMailAddress)(input.from),
100
104
  to,
101
105
  cc: normalizeList(input.cc ?? []),
@@ -125,10 +125,29 @@ function renderAccessLog(entries) {
125
125
  .reverse()
126
126
  .map((entry) => {
127
127
  const target = entry.messageId ? `message=${entry.messageId}` : entry.threadId ? `thread=${entry.threadId}` : "mailbox";
128
- return `- ${entry.accessedAt} ${entry.tool} ${target} reason="${entry.reason}"`;
128
+ const provenance = renderAccessLogProvenance(entry);
129
+ return `- ${entry.accessedAt} ${entry.tool} ${target}${provenance} reason="${entry.reason}"`;
129
130
  })
130
131
  .join("\n");
131
132
  }
133
+ function renderAccessLogProvenance(entry) {
134
+ if (entry.mailboxRole === "delegated-human-mailbox") {
135
+ return ` delegated human mailbox: ${entry.ownerEmail ?? "unknown owner"} / ${entry.source ?? "unknown source"}`;
136
+ }
137
+ if (entry.mailboxRole === "agent-native-mailbox") {
138
+ return " native agent mailbox";
139
+ }
140
+ return "";
141
+ }
142
+ function accessProvenance(message) {
143
+ const provenance = (0, core_1.describeMailProvenance)(message);
144
+ return {
145
+ mailboxRole: provenance.mailboxRole,
146
+ compartmentKind: message.compartmentKind,
147
+ ownerEmail: provenance.ownerEmail,
148
+ source: provenance.source,
149
+ };
150
+ }
132
151
  function renderSourceGrantStatus(config, agentId) {
133
152
  if (!config.registryPath) {
134
153
  return [
@@ -597,6 +616,7 @@ exports.mailToolDefinitions = [
597
616
  messageId,
598
617
  tool: "mail_thread",
599
618
  reason: args.reason,
619
+ ...accessProvenance(message),
600
620
  });
601
621
  const maxChars = numberArg(args.max_chars, 2000, 200, 6000);
602
622
  const body = decrypted.private.text.length > maxChars
@@ -717,6 +737,7 @@ exports.mailToolDefinitions = [
717
737
  messageId,
718
738
  tool: "mail_decide",
719
739
  reason,
740
+ ...accessProvenance(message),
720
741
  });
721
742
  const senderPolicyLine = persistSenderPolicyForDecision({
722
743
  registryPath: resolved.config.registryPath,
@@ -113,22 +113,28 @@ async function startMailSenseApp(options) {
113
113
  if (!resolved.ok) {
114
114
  throw new Error(resolved.error);
115
115
  }
116
- if (!resolved.config.registryPath) {
116
+ const hostedReaderOnly = resolved.storeKind === "azure-blob" && !resolved.config.registryPath;
117
+ if (!resolved.config.registryPath && !hostedReaderOnly) {
117
118
  throw new Error(`missing mailroom.registryPath for ${options.agentName}; agent-runnable repair: 'ouro connect mail --agent ${options.agentName}'`);
118
119
  }
119
- const registry = readRegistry(resolved.config.registryPath);
120
120
  const host = resolved.config.host ?? "127.0.0.1";
121
- const ingress = (options.startIngress ?? smtp_ingress_1.startMailroomIngress)({
122
- registry,
123
- store: resolved.store,
124
- smtpPort: validPort(resolved.config.smtpPort),
125
- httpPort: validPort(resolved.config.httpPort),
126
- host,
127
- });
128
- await Promise.all([
129
- waitForListening(ingress.smtp),
130
- waitForListening(ingress.health),
131
- ]);
121
+ const ingress = hostedReaderOnly
122
+ ? null
123
+ : (options.startIngress ?? smtp_ingress_1.startMailroomIngress)({
124
+ registry: readRegistry(resolved.config.registryPath),
125
+ store: resolved.store,
126
+ smtpPort: validPort(resolved.config.smtpPort),
127
+ httpPort: validPort(resolved.config.httpPort),
128
+ host,
129
+ });
130
+ if (ingress) {
131
+ await Promise.all([
132
+ waitForListening(ingress.smtp),
133
+ waitForListening(ingress.health),
134
+ ]);
135
+ }
136
+ const activeSmtpPort = () => ingress ? serverPort(ingress.smtp) : null;
137
+ const activeHttpPort = () => ingress ? serverPort(ingress.health) : null;
132
138
  const runtimePath = runtimeStatePath(options.agentName);
133
139
  const attentionPath = attentionStatePath(options.agentName);
134
140
  let lastScanAt = null;
@@ -150,8 +156,8 @@ async function startMailSenseApp(options) {
150
156
  agentName: options.agentName,
151
157
  status: "running",
152
158
  mailboxAddress: resolved.config.mailboxAddress,
153
- smtpPort: serverPort(ingress.smtp),
154
- httpPort: serverPort(ingress.health),
159
+ smtpPort: activeSmtpPort(),
160
+ httpPort: activeHttpPort(),
155
161
  host,
156
162
  storeKind: resolved.storeKind,
157
163
  storeLabel: resolved.storeLabel,
@@ -182,30 +188,32 @@ async function startMailSenseApp(options) {
182
188
  meta: {
183
189
  agentName: options.agentName,
184
190
  mailboxAddress: resolved.config.mailboxAddress,
185
- smtpPort: serverPort(ingress.smtp),
186
- httpPort: serverPort(ingress.health),
191
+ smtpPort: activeSmtpPort(),
192
+ httpPort: activeHttpPort(),
187
193
  intervalMs,
188
194
  },
189
195
  });
190
196
  return {
191
197
  runtimeStatePath: runtimePath,
192
198
  attentionStatePath: attentionPath,
193
- smtpPort: serverPort(ingress.smtp),
194
- httpPort: serverPort(ingress.health),
199
+ smtpPort: activeSmtpPort(),
200
+ httpPort: activeHttpPort(),
195
201
  async stop() {
196
202
  ;
197
203
  (options.clearIntervalFn ?? ((activeTimer) => clearInterval(activeTimer)))(timer);
198
- await Promise.all([
199
- closeServer(ingress.smtp),
200
- closeServer(ingress.health),
201
- ]);
204
+ if (ingress) {
205
+ await Promise.all([
206
+ closeServer(ingress.smtp),
207
+ closeServer(ingress.health),
208
+ ]);
209
+ }
202
210
  writeRuntimeState(runtimePath, {
203
211
  schemaVersion: 1,
204
212
  agentName: options.agentName,
205
213
  status: "stopped",
206
214
  mailboxAddress: resolved.config.mailboxAddress,
207
- smtpPort: serverPort(ingress.smtp),
208
- httpPort: serverPort(ingress.health),
215
+ smtpPort: activeSmtpPort(),
216
+ httpPort: activeHttpPort(),
209
217
  host,
210
218
  storeKind: resolved.storeKind,
211
219
  storeLabel: resolved.storeLabel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.466",
3
+ "version": "0.1.0-alpha.467",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",