@ouro.bot/cli 0.1.0-alpha.466 → 0.1.0-alpha.468
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 +15 -0
- package/dist/heart/daemon/cli-exec.js +279 -32
- package/dist/heart/daemon/cli-parse.js +71 -0
- package/dist/heart/daemon/dns-workflow.js +365 -0
- package/dist/heart/outlook/readers/mail.js +34 -1
- package/dist/mailroom/attention.js +13 -0
- package/dist/mailroom/core.js +27 -0
- package/dist/mailroom/outbound.js +4 -0
- package/dist/repertoire/tools-mail.js +22 -1
- package/dist/senses/mail.js +33 -25
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
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.468",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Published package binary smoke checks now retry transient npm registry/network failures such as `ECONNRESET`, so a successful publish is not left red by one aborted `npm exec` fetch.",
|
|
8
|
+
"`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the release-smoke retry hardening release."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"version": "0.1.0-alpha.467",
|
|
13
|
+
"changes": [
|
|
14
|
+
"`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.",
|
|
15
|
+
"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.",
|
|
16
|
+
"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."
|
|
17
|
+
]
|
|
18
|
+
},
|
|
4
19
|
{
|
|
5
20
|
"version": "0.1.0-alpha.466",
|
|
6
21
|
"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
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
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
|
-
`
|
|
3020
|
-
`
|
|
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
|
-
`
|
|
3037
|
-
`
|
|
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
|
-
`
|
|
3083
|
-
`
|
|
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
|
-
`
|
|
3098
|
-
`
|
|
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
|
-
|
|
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
|
}
|
package/dist/mailroom/core.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/dist/senses/mail.js
CHANGED
|
@@ -113,22 +113,28 @@ async function startMailSenseApp(options) {
|
|
|
113
113
|
if (!resolved.ok) {
|
|
114
114
|
throw new Error(resolved.error);
|
|
115
115
|
}
|
|
116
|
-
|
|
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 =
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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:
|
|
154
|
-
httpPort:
|
|
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:
|
|
186
|
-
httpPort:
|
|
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:
|
|
194
|
-
httpPort:
|
|
199
|
+
smtpPort: activeSmtpPort(),
|
|
200
|
+
httpPort: activeHttpPort(),
|
|
195
201
|
async stop() {
|
|
196
202
|
;
|
|
197
203
|
(options.clearIntervalFn ?? ((activeTimer) => clearInterval(activeTimer)))(timer);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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:
|
|
208
|
-
httpPort:
|
|
215
|
+
smtpPort: activeSmtpPort(),
|
|
216
|
+
httpPort: activeHttpPort(),
|
|
209
217
|
host,
|
|
210
218
|
storeKind: resolved.storeKind,
|
|
211
219
|
storeLabel: resolved.storeLabel,
|