@themoltnet/agent-daemon 0.45.0 → 0.46.0

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/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { defaultPiDaemonAdapter } from "./pi.js";
2
+ import { a as cryptoService, defaultPiDaemonAdapter, i as compileExecutionPlan, n as createExecutionPlanSnapshot, r as parseCredentialRequirements, t as runtimeExecutionOffer } from "./pi.js";
3
3
  import { assertRuntimeAdapterSupportsProfile } from "./runtime.js";
4
4
  import { Type } from "typebox";
5
5
  import "multiformats/cid";
@@ -10,21 +10,11 @@ import { dirname, join, resolve } from "node:path";
10
10
  import { parseArgs, promisify } from "node:util";
11
11
  import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, createLocalSeedSigner, resolveAgentIdentity, resolveProfileWarmSessionTtlSec, resolveRuntimeProfile, resolveRuntimeProfiles, validateRuntimeProfilePrerequisites } from "@themoltnet/agent-runtime";
12
12
  import { GuestEnvironmentBoundaryError, assertGuestEnvironmentBoundary, createPiRetryTriage, findMainWorktree, isResolvedPathInsideRoot, normalizeRetryTriageResult, redactRetryTriageSecrets } from "@themoltnet/pi-runtime";
13
+ import { connect, createNodeSecretProviderRegistry } from "@themoltnet/sdk/node";
13
14
  import { execFile, execFileSync } from "node:child_process";
14
15
  import { createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, realpathSync, rmSync } from "node:fs";
15
- import { AuthenticationError, MoltNetError, connect, createExecutorAttestor, readConfig, resolveEnvSecretReference, resolveIdentitySeed } from "@themoltnet/sdk";
16
- import { createNodeSecretProviderRegistry } from "@themoltnet/sdk/node";
16
+ import { AuthenticationError, MoltNetError, createExecutorAttestor, readConfig, resolveEnvSecretReference, resolveIdentitySeed } from "@themoltnet/sdk";
17
17
  import { createHash, randomUUID } from "node:crypto";
18
- import * as ed from "@noble/ed25519";
19
- import { createHash as createHash$1, randomBytes } from "crypto";
20
- import "multiformats/codecs/raw";
21
- import "multiformats/hashes/digest";
22
- import "@noble/hashes/sha2";
23
- import "multiformats/bases/base32";
24
- import { ed25519 } from "@noble/curves/ed25519.js";
25
- import "@ipld/dag-cbor";
26
- import "@noble/ciphers/chacha";
27
- import "@noble/hashes/hkdf";
28
18
  import { once } from "node:events";
29
19
  import { pino, transport } from "pino";
30
20
  import { metrics } from "@opentelemetry/api";
@@ -3710,6 +3700,9 @@ function loadConfig() {
3710
3700
  signingPrivateKey: process.env["MOLTNET_PRIVATE_KEY"] ?? "",
3711
3701
  signingPrivateKeyRef: process.env["MOLTNET_PRIVATE_KEY_REF"] ?? "",
3712
3702
  gitAuthor: process.env["MOLTNET_GIT_AUTHOR"] ?? "",
3703
+ profileCredentialRequirements: process.env["MOLTNET_PROFILE_CREDENTIAL_REQUIREMENTS"] ?? "",
3704
+ credentialBindings: process.env["MOLTNET_CREDENTIAL_BINDINGS"] ?? "",
3705
+ credentialEnforcement: process.env["MOLTNET_CREDENTIAL_ENFORCEMENT"] ?? "",
3713
3706
  traceIdlePolling: readBoolean("MOLTNET_TRACE_IDLE_POLLING", process.env["MOLTNET_TRACE_IDLE_POLLING"])
3714
3707
  };
3715
3708
  }
@@ -4288,172 +4281,6 @@ function recoverScratchWorkspacePath(producer, stateDirs) {
4288
4281
  return existsSync(fallback) ? fallback : null;
4289
4282
  }
4290
4283
  //#endregion
4291
- //#region ../../libs/crypto-service/src/ssh.ts
4292
- /**
4293
- * SSH key format conversion for MoltNet Ed25519 keys
4294
- *
4295
- * Converts MoltNet agent keys (ed25519:<base64>) to OpenSSH format
4296
- * for use with git commit signing and SSH authentication.
4297
- */
4298
- if (!ed.etc.sha512Sync) ed.etc.sha512Sync = (...m) => {
4299
- const hash = createHash$1("sha512");
4300
- m.forEach((msg) => hash.update(msg));
4301
- return hash.digest();
4302
- };
4303
- new TextEncoder();
4304
- //#endregion
4305
- //#region ../../libs/crypto-service/src/crypto.service.ts
4306
- /**
4307
- * MoltNet Crypto Service
4308
- *
4309
- * Ed25519 cryptographic operations for agent identity
4310
- * Uses @noble/ed25519 for pure TypeScript implementation
4311
- */
4312
- ed.etc.sha512Sync = (...m) => {
4313
- const hash = createHash$1("sha512");
4314
- m.forEach((msg) => hash.update(msg));
4315
- return hash.digest();
4316
- };
4317
- /** Domain-separation prefix for the signing payload envelope. */
4318
- var DOMAIN_PREFIX = "moltnet:v1";
4319
- /**
4320
- * Build deterministic signing bytes with domain separation and
4321
- * length-prefixed binary framing.
4322
- *
4323
- * Layout:
4324
- * UTF-8("moltnet:v1") || u32be(len(msg_hash)) || msg_hash || u32be(len(nonce_bytes)) || nonce_bytes
4325
- *
4326
- * Where msg_hash = SHA-256(UTF-8(message)).
4327
- *
4328
- * This produces a fixed-structure byte sequence immune to whitespace,
4329
- * newline, and encoding differences between runtimes.
4330
- */
4331
- function buildSigningBytes(message, nonce) {
4332
- const msgHash = createHash$1("sha256").update(Buffer.from(message, "utf-8")).digest();
4333
- const nonceBytes = Buffer.from(nonce, "utf-8");
4334
- const prefix = Buffer.from(DOMAIN_PREFIX, "utf-8");
4335
- const buf = Buffer.alloc(prefix.length + 4 + msgHash.length + 4 + nonceBytes.length);
4336
- let offset = 0;
4337
- prefix.copy(buf, offset);
4338
- offset += prefix.length;
4339
- buf.writeUInt32BE(msgHash.length, offset);
4340
- offset += 4;
4341
- msgHash.copy(buf, offset);
4342
- offset += msgHash.length;
4343
- buf.writeUInt32BE(nonceBytes.length, offset);
4344
- offset += 4;
4345
- nonceBytes.copy(buf, offset);
4346
- return new Uint8Array(buf);
4347
- }
4348
- var cryptoService = {
4349
- async generateKeyPair() {
4350
- const privateKeyBytes = ed.utils.randomPrivateKey();
4351
- const publicKeyBytes = await ed.getPublicKeyAsync(privateKeyBytes);
4352
- const privateKey = Buffer.from(privateKeyBytes).toString("base64");
4353
- return {
4354
- publicKey: `ed25519:${Buffer.from(publicKeyBytes).toString("base64")}`,
4355
- privateKey,
4356
- fingerprint: this.generateFingerprint(publicKeyBytes)
4357
- };
4358
- },
4359
- generateFingerprint(publicKeyBytes) {
4360
- return (createHash$1("sha256").update(publicKeyBytes).digest("hex").slice(0, 16).toUpperCase().match(/.{4}/g) ?? []).join("-");
4361
- },
4362
- parsePublicKey(publicKey) {
4363
- const base64 = publicKey.replace(/^ed25519:/, "");
4364
- return new Uint8Array(Buffer.from(base64, "base64"));
4365
- },
4366
- async sign(message, privateKeyBase64) {
4367
- const privateKeyBytes = new Uint8Array(Buffer.from(privateKeyBase64, "base64"));
4368
- const messageBytes = new TextEncoder().encode(message);
4369
- const signature = await ed.signAsync(messageBytes, privateKeyBytes);
4370
- return Buffer.from(signature).toString("base64");
4371
- },
4372
- async verify(message, signature, publicKey) {
4373
- try {
4374
- const publicKeyBytes = this.parsePublicKey(publicKey);
4375
- const signatureBytes = new Uint8Array(Buffer.from(signature, "base64"));
4376
- const messageBytes = new TextEncoder().encode(message);
4377
- return await ed.verifyAsync(signatureBytes, messageBytes, publicKeyBytes);
4378
- } catch {
4379
- return false;
4380
- }
4381
- },
4382
- async signWithNonce(message, nonce, privateKeyBase64) {
4383
- const privateKeyBytes = new Uint8Array(Buffer.from(privateKeyBase64, "base64"));
4384
- const signingBytes = buildSigningBytes(message, nonce);
4385
- const signature = await ed.signAsync(signingBytes, privateKeyBytes);
4386
- return Buffer.from(signature).toString("base64");
4387
- },
4388
- async verifyWithNonce(message, nonce, signature, publicKey) {
4389
- try {
4390
- const publicKeyBytes = this.parsePublicKey(publicKey);
4391
- const signatureBytes = new Uint8Array(Buffer.from(signature, "base64"));
4392
- const signingBytes = buildSigningBytes(message, nonce);
4393
- return await ed.verifyAsync(signatureBytes, signingBytes, publicKeyBytes);
4394
- } catch {
4395
- return false;
4396
- }
4397
- },
4398
- async createSignedMessage(message, privateKeyBase64, publicKey) {
4399
- return {
4400
- message,
4401
- signature: await this.sign(message, privateKeyBase64),
4402
- publicKey
4403
- };
4404
- },
4405
- async verifySignedMessage(signedMessage) {
4406
- return this.verify(signedMessage.message, signedMessage.signature, signedMessage.publicKey);
4407
- },
4408
- generateChallenge() {
4409
- return `moltnet:challenge:${randomBytes(32).toString("hex")}:${Date.now()}`;
4410
- },
4411
- async derivePublicKey(privateKeyBase64) {
4412
- const privateKeyBytes = new Uint8Array(Buffer.from(privateKeyBase64, "base64"));
4413
- const publicKeyBytes = await ed.getPublicKeyAsync(privateKeyBytes);
4414
- return `ed25519:${Buffer.from(publicKeyBytes).toString("base64")}`;
4415
- },
4416
- getFingerprintFromPublicKey(publicKey) {
4417
- const publicKeyBytes = this.parsePublicKey(publicKey);
4418
- return this.generateFingerprint(publicKeyBytes);
4419
- },
4420
- deriveX25519PrivateKey(ed25519PrivateKeyBase64) {
4421
- const seed = new Uint8Array(Buffer.from(ed25519PrivateKeyBase64, "base64"));
4422
- const x25519Priv = ed25519.utils.toMontgomerySecret(seed);
4423
- return Buffer.from(x25519Priv).toString("base64");
4424
- },
4425
- deriveX25519PublicKey(ed25519PublicKey) {
4426
- const edPubBytes = this.parsePublicKey(ed25519PublicKey);
4427
- const x25519Pub = ed25519.utils.toMontgomery(edPubBytes);
4428
- return `x25519:${Buffer.from(x25519Pub).toString("base64")}`;
4429
- },
4430
- async createIdentityProof(identityId, privateKeyBase64) {
4431
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
4432
- const message = `moltnet:register:${identityId}:${timestamp}`;
4433
- return {
4434
- message,
4435
- signature: await this.sign(message, privateKeyBase64),
4436
- timestamp
4437
- };
4438
- },
4439
- async verifyIdentityProof(proof, publicKey, expectedIdentityId) {
4440
- if (!await this.verify(proof.message, proof.signature, publicKey)) return false;
4441
- const expectedPrefix = `moltnet:register:${expectedIdentityId}:`;
4442
- if (!proof.message.startsWith(expectedPrefix)) return false;
4443
- const proofTime = new Date(proof.timestamp).getTime();
4444
- if (Date.now() - proofTime > 300 * 1e3) return false;
4445
- return true;
4446
- }
4447
- };
4448
- //#endregion
4449
- //#region ../../libs/crypto-service/src/executor-attestation.ts
4450
- ed.etc.sha512Sync = (...m) => {
4451
- const hash = createHash("sha512");
4452
- m.forEach((msg) => hash.update(msg));
4453
- return hash.digest();
4454
- };
4455
- new TextEncoder().encode("SSHSIG");
4456
- //#endregion
4457
4284
  //#region src/lib/executor-attestation.ts
4458
4285
  var DAEMON_REQUIRED_SCOPES = AGENT_CREDENTIAL_SCOPES;
4459
4286
  async function resolveExecutorSigningPrivateKey(input) {
@@ -4859,6 +4686,257 @@ async function maybeWriteAnchors(output, ctx) {
4859
4686
  }
4860
4687
  }
4861
4688
  //#endregion
4689
+ //#region ../../libs/execution-integrations/src/credential-broker.ts
4690
+ async function checkCredentialReadiness(requirements, bindings, providers) {
4691
+ const records = [];
4692
+ for (const requirement of requirements) {
4693
+ const binding = ownBinding(bindings, requirement.name);
4694
+ if (binding === void 0) {
4695
+ records.push({
4696
+ name: requirement.name,
4697
+ required: requirement.required,
4698
+ status: requirement.required ? "required_binding_missing" : "binding_absent"
4699
+ });
4700
+ continue;
4701
+ }
4702
+ const bindingDigest = await digestBindingReference(binding.reference);
4703
+ const source = binding.source ?? "local-activation";
4704
+ if (providers.get(binding.reference.provider) === void 0) {
4705
+ records.push({
4706
+ name: requirement.name,
4707
+ required: requirement.required,
4708
+ status: "provider_unavailable",
4709
+ bindingDigest,
4710
+ source
4711
+ });
4712
+ continue;
4713
+ }
4714
+ const probe = await providers.probe(binding.reference);
4715
+ records.push({
4716
+ name: requirement.name,
4717
+ required: requirement.required,
4718
+ status: probe === "present" ? "ready" : probe === "absent" ? "binding_absent" : "host_store_inaccessible",
4719
+ bindingDigest,
4720
+ source
4721
+ });
4722
+ }
4723
+ return records;
4724
+ }
4725
+ async function digestBindingReference(reference) {
4726
+ const bytes = new TextEncoder().encode(`${reference.provider}\u0000${reference.key}`);
4727
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
4728
+ return `sha256:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, 16)}`;
4729
+ }
4730
+ function ownBinding(bindings, name) {
4731
+ return Object.hasOwn(bindings, name) ? bindings[name] : void 0;
4732
+ }
4733
+ //#endregion
4734
+ //#region ../../libs/execution-integrations/src/runtime-profile.ts
4735
+ var CURRENT_EFFECTIVE_POLICY_SNAPSHOT_VERSION = "effective-policy:v1";
4736
+ /**
4737
+ * Maps resolved product authority into portable intent. Policy composition is
4738
+ * deliberately upstream: this boundary accepts no policy IDs and performs no
4739
+ * union, lookup, or precedence handling.
4740
+ */
4741
+ function executionIntentFromRuntimeProfile(input) {
4742
+ if (input.policy.snapshot.runtimeKind !== input.profile.runtimeKind) throw new Error("resolved policy authority does not match runtime profile");
4743
+ const network = input.profile.sandboxConfig.network;
4744
+ return {
4745
+ mode: input.mode,
4746
+ profile: {
4747
+ id: input.profile.id,
4748
+ revision: input.profileRevision,
4749
+ definitionCid: input.profile.definitionCid
4750
+ },
4751
+ authority: {
4752
+ policySnapshotHash: input.policy.hash,
4753
+ policySnapshotVersion: input.policy.snapshot.version,
4754
+ ...input.policy.authorizedControls !== void 0 && { authorizedControls: [...input.policy.authorizedControls] }
4755
+ },
4756
+ credentialRequirements: structuredClone(input.credentialRequirements),
4757
+ requiredCapabilities: [...input.requiredCapabilities ?? []],
4758
+ lease: {
4759
+ ttlSec: input.profile.leaseTtlSec,
4760
+ requiredControls: [...input.requiredLeaseControls ?? []]
4761
+ },
4762
+ network: {
4763
+ allowedHosts: [...network?.allowedHosts ?? []],
4764
+ allowedInternalHosts: [...network?.allowedInternalHosts ?? []]
4765
+ },
4766
+ provenance: {
4767
+ profile: input.profile.source,
4768
+ policy: `runtime-policy-snapshot:${input.policy.hash}`,
4769
+ requirements: input.requirementsProvenance
4770
+ }
4771
+ };
4772
+ }
4773
+ //#endregion
4774
+ //#region src/lib/governance-plan.ts
4775
+ /**
4776
+ * Observe-only governance-plan compilation (#1970 private slice).
4777
+ *
4778
+ * Distinct from `task-execution-plan.ts` (workspace/session planning): this
4779
+ * module compiles the CREDENTIAL/CAPABILITY governance plan from trusted
4780
+ * local configuration and logs its value-free decisions. It gates nothing
4781
+ * yet — enforcement is a separate, later change.
4782
+ */
4783
+ var DEFAULT_GOVERNANCE_OBSERVE_TIMEOUT_MS = 2e3;
4784
+ /**
4785
+ * Two distinct provenance sources (#2022 review): requirements come from a
4786
+ * profile-side/private input (`MOLTNET_PROFILE_CREDENTIAL_REQUIREMENTS`, a
4787
+ * JSON map of profile id → requirements) and bindings from trusted
4788
+ * local/deployment configuration (`MOLTNET_CREDENTIAL_BINDINGS`). Both raw
4789
+ * values are read once in config.ts. Both empty → null (observer disabled).
4790
+ * Malformed configuration throws — a truncated credential policy must never
4791
+ * be silently ignored.
4792
+ */
4793
+ function loadRuntimeCredentialConfig(raw) {
4794
+ const hasRequirements = raw.profileRequirements.trim() !== "";
4795
+ const hasBindings = raw.bindings.trim() !== "";
4796
+ if (!hasRequirements && !hasBindings) return null;
4797
+ const requirementsByProfile = {};
4798
+ if (hasRequirements) {
4799
+ const parsed = JSON.parse(raw.profileRequirements);
4800
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("MOLTNET_PROFILE_CREDENTIAL_REQUIREMENTS must be a JSON object keyed by profile id");
4801
+ for (const [profileId, value] of Object.entries(parsed)) requirementsByProfile[profileId] = parseCredentialRequirements(value);
4802
+ }
4803
+ const bindings = {};
4804
+ if (hasBindings) {
4805
+ const parsed = JSON.parse(raw.bindings);
4806
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("MOLTNET_CREDENTIAL_BINDINGS must be a JSON object");
4807
+ for (const [name, binding] of Object.entries(parsed)) {
4808
+ if (typeof binding?.reference?.provider !== "string" || typeof binding.reference.key !== "string") throw new Error(`MOLTNET_CREDENTIAL_BINDINGS binding "${name}" must carry a { reference: { provider, key } }`);
4809
+ bindings[name] = {
4810
+ ...binding,
4811
+ source: "local-bindings-config"
4812
+ };
4813
+ }
4814
+ }
4815
+ return {
4816
+ requirementsByProfile,
4817
+ bindings,
4818
+ sources: {
4819
+ requirements: "profile-requirements-config",
4820
+ bindings: "local-bindings-config"
4821
+ }
4822
+ };
4823
+ }
4824
+ /**
4825
+ * Resolve the credential-governance enforcement mode, mirroring the runtime
4826
+ * tool-policy `off | watch | enforce` ladder. Unset defaults to `watch` when
4827
+ * requirement/binding sources are configured, `off` otherwise. `enforce` is
4828
+ * rejected until the enforcement flip (skip-before-claim) is implemented —
4829
+ * accepting it as a silent watch would misrepresent the deployment.
4830
+ */
4831
+ function resolveCredentialEnforcement(raw, sources) {
4832
+ const configured = sources.profileRequirements.trim() !== "" || sources.bindings.trim() !== "";
4833
+ const value = raw.trim();
4834
+ if (value === "") return configured ? "watch" : "off";
4835
+ if (value === "off" || value === "watch") return value;
4836
+ if (value === "enforce") throw new Error("MOLTNET_CREDENTIAL_ENFORCEMENT=enforce is not implemented yet: the skip-before-claim flip is a separate change. Use watch (audit only).");
4837
+ throw new Error(`MOLTNET_CREDENTIAL_ENFORCEMENT must be off, watch or enforce; got "${value}"`);
4838
+ }
4839
+ async function observeGovernancePlan(input) {
4840
+ const unavailableReason = authorityUnavailableReason(input);
4841
+ if (unavailableReason !== null) {
4842
+ (input.logger.warn ?? input.logger.info).call(input.logger, {
4843
+ mode: "observe",
4844
+ taskId: input.taskId,
4845
+ attemptN: input.attemptN,
4846
+ profileId: input.profile.id,
4847
+ reason: unavailableReason
4848
+ }, "agent-daemon.governance_plan_unavailable");
4849
+ return {
4850
+ status: "unavailable",
4851
+ snapshot: null,
4852
+ reason: unavailableReason
4853
+ };
4854
+ }
4855
+ const { runtimeProfileRevision, policySnapshotHash } = input.claimAuthority;
4856
+ if (runtimeProfileRevision === void 0 || policySnapshotHash === void 0) throw new Error("validated claim authority is missing immutable pins");
4857
+ if (input.offer === void 0) throw new Error("validated execution offer is missing");
4858
+ const requirements = input.config.requirementsByProfile[input.profile.id] ?? [];
4859
+ const credentialReadiness = await checkCredentialReadiness(requirements, input.config.bindings, input.registry);
4860
+ const intent = executionIntentFromRuntimeProfile({
4861
+ mode: "watch",
4862
+ profile: input.profile,
4863
+ profileRevision: runtimeProfileRevision,
4864
+ policy: {
4865
+ hash: policySnapshotHash,
4866
+ snapshot: {
4867
+ version: CURRENT_EFFECTIVE_POLICY_SNAPSHOT_VERSION,
4868
+ runtimeKind: input.profile.runtimeKind
4869
+ },
4870
+ authorizedControls: void 0
4871
+ },
4872
+ credentialRequirements: requirements,
4873
+ requiredCapabilities: [],
4874
+ requirementsProvenance: input.config.sources.requirements
4875
+ });
4876
+ const plan = compileExecutionPlan({
4877
+ intent,
4878
+ offer: input.offer,
4879
+ credentialReadiness
4880
+ });
4881
+ const snapshot = await createExecutionPlanSnapshot({
4882
+ intent,
4883
+ offer: input.offer,
4884
+ credentialReadiness,
4885
+ plan
4886
+ });
4887
+ const logObject = {
4888
+ mode: "observe",
4889
+ taskId: input.taskId,
4890
+ attemptN: input.attemptN,
4891
+ profileId: input.profile.id,
4892
+ sources: input.config.sources,
4893
+ snapshotCid: snapshot.cid,
4894
+ profileRevision: snapshot.intent.profile.revision,
4895
+ policySnapshotHash: snapshot.intent.authority.policySnapshotHash,
4896
+ launchable: plan.launchable,
4897
+ decisions: plan.decisions,
4898
+ credentialReadiness
4899
+ };
4900
+ if (plan.launchable) input.logger.info(logObject, "agent-daemon.governance_plan");
4901
+ else (input.logger.warn ?? input.logger.info).call(input.logger, logObject, "agent-daemon.governance_plan_would_block");
4902
+ return {
4903
+ status: "observed",
4904
+ snapshot
4905
+ };
4906
+ }
4907
+ /**
4908
+ * Observe without allowing a stuck keyring/provider to delay task execution.
4909
+ * The observer is intentionally non-gating: failures and deadlines are logged
4910
+ * with claim correlation and converted to `null`.
4911
+ */
4912
+ async function observeGovernancePlanSafely(input, timeoutMs = DEFAULT_GOVERNANCE_OBSERVE_TIMEOUT_MS) {
4913
+ let timeout;
4914
+ try {
4915
+ return await Promise.race([observeGovernancePlan(input), new Promise((_resolve, reject) => {
4916
+ timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("governance observation timed out")), timeoutMs);
4917
+ })]);
4918
+ } catch (error) {
4919
+ (input.logger.warn ?? input.logger.info).call(input.logger, {
4920
+ mode: "observe",
4921
+ taskId: input.taskId,
4922
+ attemptN: input.attemptN,
4923
+ err: error instanceof Error ? error.message : String(error)
4924
+ }, "agent-daemon.governance_plan_observe_failed");
4925
+ return null;
4926
+ } finally {
4927
+ if (timeout !== void 0) clearTimeout(timeout);
4928
+ }
4929
+ }
4930
+ function authorityUnavailableReason(input) {
4931
+ const authority = input.claimAuthority;
4932
+ if (authority.runtimeProfileId === void 0 || authority.runtimeProfileRevision === void 0 || authority.policySnapshotHash === void 0 || authority.executorFingerprint === void 0) return "claim_authority_unpinned";
4933
+ if (authority.runtimeProfileId !== input.profile.id) return "runtime_profile_mismatch";
4934
+ if (authority.executorFingerprint !== input.executorFingerprint) return "executor_fingerprint_mismatch";
4935
+ if (input.offer === void 0) return "execution_offer_unavailable";
4936
+ if (input.offer.executor.fingerprint !== input.executorFingerprint) return "executor_fingerprint_mismatch";
4937
+ return null;
4938
+ }
4939
+ //#endregion
4862
4940
  //#region src/lib/logger.ts
4863
4941
  /**
4864
4942
  * Logger setup + teardown for the agent-daemon CLI commands.
@@ -5696,6 +5774,11 @@ async function runPolling(opts) {
5696
5774
  }
5697
5775
  if (taskTypes.length === 0) console.error(`[${opts.modeLabel}] --task-types is empty — daemon will accept any registered type. Pass an explicit list to limit scope (e.g. --task-types fulfill_brief).`);
5698
5776
  const cfg = loadConfig();
5777
+ const credentialSources = {
5778
+ profileRequirements: cfg.profileCredentialRequirements,
5779
+ bindings: cfg.credentialBindings
5780
+ };
5781
+ const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
5699
5782
  const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
5700
5783
  const { ctx, signingPrivateKey, startupWhoami, agentIdentity, hostCapabilitySigner } = await (async () => {
5701
5784
  let gate = "resolve_agent_context";
@@ -6062,6 +6145,17 @@ async function runPolling(opts) {
6062
6145
  executeTask: async (claimedTask, reporter) => {
6063
6146
  const selected = runtimeForClaimedTask(runtimes, claimedTask);
6064
6147
  const { common, executionPlans, profile, sandbox, slotIdentity, stateDirs } = selected;
6148
+ if (runtimeCredentialConfig) await observeGovernancePlanSafely({
6149
+ config: runtimeCredentialConfig,
6150
+ profile,
6151
+ offer: runtimeExecutionOffer(selected.preparedRuntime, selected.preparedRuntime.attestor.fingerprint),
6152
+ registry: createNodeSecretProviderRegistry(),
6153
+ executorFingerprint: selected.preparedRuntime.attestor.fingerprint,
6154
+ claimAuthority: claimedTask.claimAuthority ?? {},
6155
+ taskId: claimedTask.task.id,
6156
+ attemptN: claimedTask.attemptN,
6157
+ logger: rootLogger
6158
+ });
6065
6159
  const taskLogger = rootLogger.child({
6066
6160
  runtimeProfileId: profile.id,
6067
6161
  runtimeProfileName: profile.name,
@@ -6332,6 +6426,11 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6332
6426
  return 1;
6333
6427
  }
6334
6428
  const cfg = loadConfig();
6429
+ const credentialSources = {
6430
+ profileRequirements: cfg.profileCredentialRequirements,
6431
+ bindings: cfg.credentialBindings
6432
+ };
6433
+ const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
6335
6434
  const initialOpts = opts;
6336
6435
  const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
6337
6436
  const { ctx, signingPrivateKey, agentIdentity, hostCapabilitySigner } = await (async () => {
@@ -6581,6 +6680,17 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6581
6680
  maxBashTimeouts: opts.maxBashTimeouts
6582
6681
  });
6583
6682
  const executeTask = async (claimedTask, reporter) => {
6683
+ if (runtimeCredentialConfig) await observeGovernancePlanSafely({
6684
+ config: runtimeCredentialConfig,
6685
+ profile,
6686
+ offer: runtimeExecutionOffer(preparedRuntime, preparedRuntime.attestor.fingerprint),
6687
+ registry: createNodeSecretProviderRegistry(),
6688
+ executorFingerprint: preparedRuntime.attestor.fingerprint,
6689
+ claimAuthority: claimedTask.claimAuthority ?? {},
6690
+ taskId: claimedTask.task.id,
6691
+ attemptN: claimedTask.attemptN,
6692
+ logger: rootLogger
6693
+ });
6584
6694
  let executionPlan;
6585
6695
  try {
6586
6696
  executionPlan = await executionPlans.getOrCreate(claimedTask);
package/dist/pi.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"pi.d.ts","sourceRoot":"","sources":["../src/pi.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,mBAAmB,EACzB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,oBAAoB,EAAyB,MAAM,cAAc,CAAC;AAEhF,eAAO,MAAM,oBAAoB,kkBAIvB,CAAC;AAEX,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,mBAAmB,GAC3B,oBAAoB,CA4DtB;AAED,eAAO,MAAM,0BAA0B,qBASrC,CAAC;AAEH,eAAO,MAAM,sBAAsB,sBAElC,CAAC"}
1
+ {"version":3,"file":"pi.d.ts","sourceRoot":"","sources":["../src/pi.ts"],"names":[],"mappings":"AACA,OAAO,EASL,KAAK,mBAAmB,EACzB,MAAM,wBAAwB,CAAC;AAGhC,OAAO,KAAK,EAAE,oBAAoB,EAAyB,MAAM,cAAc,CAAC;AAEhF,eAAO,MAAM,oBAAoB,kkBAIvB,CAAC;AAEX,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,mBAAmB,GAC3B,oBAAoB,CAkEtB;AAED,eAAO,MAAM,0BAA0B,qBASrC,CAAC;AAEH,eAAO,MAAM,sBAAsB,sBAElC,CAAC"}
package/dist/pi.js CHANGED
@@ -1,5 +1,558 @@
1
1
  #!/usr/bin/env node
2
+ import { Type } from "typebox";
3
+ import { CID } from "multiformats/cid";
4
+ import * as json from "multiformats/codecs/json";
5
+ import { sha256 } from "multiformats/hashes/sha2";
6
+ import { Value } from "typebox/value";
2
7
  import { GONDOLIN_BASE_EXECUTABLES, GONDOLIN_TOOL_NAMES, MOLTNET_TOOL_NAMES, agentSigningCapability, buildPiExecutorManifest, createPiTaskExecutor, defineGondolinTemplate, definePiRuntime } from "@themoltnet/pi-runtime";
8
+ import { createHash } from "node:crypto";
9
+ import * as ed from "@noble/ed25519";
10
+ import { createHash as createHash$1, randomBytes } from "crypto";
11
+ import "multiformats/codecs/raw";
12
+ import "multiformats/hashes/digest";
13
+ import "@noble/hashes/sha2";
14
+ import "multiformats/bases/base32";
15
+ import { ed25519 } from "@noble/curves/ed25519.js";
16
+ import "@ipld/dag-cbor";
17
+ import "@noble/ciphers/chacha";
18
+ import "@noble/hashes/hkdf";
19
+ //#region ../../libs/crypto-service/src/ssh.ts
20
+ /**
21
+ * SSH key format conversion for MoltNet Ed25519 keys
22
+ *
23
+ * Converts MoltNet agent keys (ed25519:<base64>) to OpenSSH format
24
+ * for use with git commit signing and SSH authentication.
25
+ */
26
+ if (!ed.etc.sha512Sync) ed.etc.sha512Sync = (...m) => {
27
+ const hash = createHash$1("sha512");
28
+ m.forEach((msg) => hash.update(msg));
29
+ return hash.digest();
30
+ };
31
+ new TextEncoder();
32
+ //#endregion
33
+ //#region ../../libs/crypto-service/src/crypto.service.ts
34
+ /**
35
+ * MoltNet Crypto Service
36
+ *
37
+ * Ed25519 cryptographic operations for agent identity
38
+ * Uses @noble/ed25519 for pure TypeScript implementation
39
+ */
40
+ ed.etc.sha512Sync = (...m) => {
41
+ const hash = createHash$1("sha512");
42
+ m.forEach((msg) => hash.update(msg));
43
+ return hash.digest();
44
+ };
45
+ /** Domain-separation prefix for the signing payload envelope. */
46
+ var DOMAIN_PREFIX = "moltnet:v1";
47
+ /**
48
+ * Build deterministic signing bytes with domain separation and
49
+ * length-prefixed binary framing.
50
+ *
51
+ * Layout:
52
+ * UTF-8("moltnet:v1") || u32be(len(msg_hash)) || msg_hash || u32be(len(nonce_bytes)) || nonce_bytes
53
+ *
54
+ * Where msg_hash = SHA-256(UTF-8(message)).
55
+ *
56
+ * This produces a fixed-structure byte sequence immune to whitespace,
57
+ * newline, and encoding differences between runtimes.
58
+ */
59
+ function buildSigningBytes(message, nonce) {
60
+ const msgHash = createHash$1("sha256").update(Buffer.from(message, "utf-8")).digest();
61
+ const nonceBytes = Buffer.from(nonce, "utf-8");
62
+ const prefix = Buffer.from(DOMAIN_PREFIX, "utf-8");
63
+ const buf = Buffer.alloc(prefix.length + 4 + msgHash.length + 4 + nonceBytes.length);
64
+ let offset = 0;
65
+ prefix.copy(buf, offset);
66
+ offset += prefix.length;
67
+ buf.writeUInt32BE(msgHash.length, offset);
68
+ offset += 4;
69
+ msgHash.copy(buf, offset);
70
+ offset += msgHash.length;
71
+ buf.writeUInt32BE(nonceBytes.length, offset);
72
+ offset += 4;
73
+ nonceBytes.copy(buf, offset);
74
+ return new Uint8Array(buf);
75
+ }
76
+ var cryptoService = {
77
+ async generateKeyPair() {
78
+ const privateKeyBytes = ed.utils.randomPrivateKey();
79
+ const publicKeyBytes = await ed.getPublicKeyAsync(privateKeyBytes);
80
+ const privateKey = Buffer.from(privateKeyBytes).toString("base64");
81
+ return {
82
+ publicKey: `ed25519:${Buffer.from(publicKeyBytes).toString("base64")}`,
83
+ privateKey,
84
+ fingerprint: this.generateFingerprint(publicKeyBytes)
85
+ };
86
+ },
87
+ generateFingerprint(publicKeyBytes) {
88
+ return (createHash$1("sha256").update(publicKeyBytes).digest("hex").slice(0, 16).toUpperCase().match(/.{4}/g) ?? []).join("-");
89
+ },
90
+ parsePublicKey(publicKey) {
91
+ const base64 = publicKey.replace(/^ed25519:/, "");
92
+ return new Uint8Array(Buffer.from(base64, "base64"));
93
+ },
94
+ async sign(message, privateKeyBase64) {
95
+ const privateKeyBytes = new Uint8Array(Buffer.from(privateKeyBase64, "base64"));
96
+ const messageBytes = new TextEncoder().encode(message);
97
+ const signature = await ed.signAsync(messageBytes, privateKeyBytes);
98
+ return Buffer.from(signature).toString("base64");
99
+ },
100
+ async verify(message, signature, publicKey) {
101
+ try {
102
+ const publicKeyBytes = this.parsePublicKey(publicKey);
103
+ const signatureBytes = new Uint8Array(Buffer.from(signature, "base64"));
104
+ const messageBytes = new TextEncoder().encode(message);
105
+ return await ed.verifyAsync(signatureBytes, messageBytes, publicKeyBytes);
106
+ } catch {
107
+ return false;
108
+ }
109
+ },
110
+ async signWithNonce(message, nonce, privateKeyBase64) {
111
+ const privateKeyBytes = new Uint8Array(Buffer.from(privateKeyBase64, "base64"));
112
+ const signingBytes = buildSigningBytes(message, nonce);
113
+ const signature = await ed.signAsync(signingBytes, privateKeyBytes);
114
+ return Buffer.from(signature).toString("base64");
115
+ },
116
+ async verifyWithNonce(message, nonce, signature, publicKey) {
117
+ try {
118
+ const publicKeyBytes = this.parsePublicKey(publicKey);
119
+ const signatureBytes = new Uint8Array(Buffer.from(signature, "base64"));
120
+ const signingBytes = buildSigningBytes(message, nonce);
121
+ return await ed.verifyAsync(signatureBytes, signingBytes, publicKeyBytes);
122
+ } catch {
123
+ return false;
124
+ }
125
+ },
126
+ async createSignedMessage(message, privateKeyBase64, publicKey) {
127
+ return {
128
+ message,
129
+ signature: await this.sign(message, privateKeyBase64),
130
+ publicKey
131
+ };
132
+ },
133
+ async verifySignedMessage(signedMessage) {
134
+ return this.verify(signedMessage.message, signedMessage.signature, signedMessage.publicKey);
135
+ },
136
+ generateChallenge() {
137
+ return `moltnet:challenge:${randomBytes(32).toString("hex")}:${Date.now()}`;
138
+ },
139
+ async derivePublicKey(privateKeyBase64) {
140
+ const privateKeyBytes = new Uint8Array(Buffer.from(privateKeyBase64, "base64"));
141
+ const publicKeyBytes = await ed.getPublicKeyAsync(privateKeyBytes);
142
+ return `ed25519:${Buffer.from(publicKeyBytes).toString("base64")}`;
143
+ },
144
+ getFingerprintFromPublicKey(publicKey) {
145
+ const publicKeyBytes = this.parsePublicKey(publicKey);
146
+ return this.generateFingerprint(publicKeyBytes);
147
+ },
148
+ deriveX25519PrivateKey(ed25519PrivateKeyBase64) {
149
+ const seed = new Uint8Array(Buffer.from(ed25519PrivateKeyBase64, "base64"));
150
+ const x25519Priv = ed25519.utils.toMontgomerySecret(seed);
151
+ return Buffer.from(x25519Priv).toString("base64");
152
+ },
153
+ deriveX25519PublicKey(ed25519PublicKey) {
154
+ const edPubBytes = this.parsePublicKey(ed25519PublicKey);
155
+ const x25519Pub = ed25519.utils.toMontgomery(edPubBytes);
156
+ return `x25519:${Buffer.from(x25519Pub).toString("base64")}`;
157
+ },
158
+ async createIdentityProof(identityId, privateKeyBase64) {
159
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
160
+ const message = `moltnet:register:${identityId}:${timestamp}`;
161
+ return {
162
+ message,
163
+ signature: await this.sign(message, privateKeyBase64),
164
+ timestamp
165
+ };
166
+ },
167
+ async verifyIdentityProof(proof, publicKey, expectedIdentityId) {
168
+ if (!await this.verify(proof.message, proof.signature, publicKey)) return false;
169
+ const expectedPrefix = `moltnet:register:${expectedIdentityId}:`;
170
+ if (!proof.message.startsWith(expectedPrefix)) return false;
171
+ const proofTime = new Date(proof.timestamp).getTime();
172
+ if (Date.now() - proofTime > 300 * 1e3) return false;
173
+ return true;
174
+ }
175
+ };
176
+ //#endregion
177
+ //#region ../../libs/crypto-service/src/executor-attestation.ts
178
+ ed.etc.sha512Sync = (...m) => {
179
+ const hash = createHash("sha512");
180
+ m.forEach((msg) => hash.update(msg));
181
+ return hash.digest();
182
+ };
183
+ //#endregion
184
+ //#region ../../libs/crypto-service/src/json-cid.ts
185
+ /**
186
+ * Generic JSON CID — CIDv1 for arbitrary JSON-serialisable values.
187
+ *
188
+ * Uses the dag-json codec and sha2-256, producing a base32lower CIDv1.
189
+ * Suitable for content-addressing task inputs, schema objects, and other
190
+ * JSON payloads that don't need diary-entry canonical normalisation.
191
+ */
192
+ async function computeJsonCid(value) {
193
+ const bytes = json.encode(value);
194
+ const hash = await sha256.digest(bytes);
195
+ return CID.create(1, json.code, hash).toString();
196
+ }
197
+ new TextEncoder().encode("SSHSIG");
198
+ //#endregion
199
+ //#region ../../libs/execution-plan/src/control-ids.ts
200
+ var CREDENTIAL_AUTHORITY_PREFIX = "credential:";
201
+ var CREDENTIAL_PROJECTION_PREFIX = "credential-projection:";
202
+ var HOST_CAPABILITY_PREFIX = "host-capability:";
203
+ function credentialAuthorityControl(name) {
204
+ return `${CREDENTIAL_AUTHORITY_PREFIX}${name}`;
205
+ }
206
+ function credentialProjectionControl(projection) {
207
+ return `${CREDENTIAL_PROJECTION_PREFIX}${projection}`;
208
+ }
209
+ function hostCapabilityControl(name) {
210
+ return `${HOST_CAPABILITY_PREFIX}${name}`;
211
+ }
212
+ //#endregion
213
+ //#region ../../libs/execution-plan/src/compile-execution-plan.ts
214
+ /**
215
+ * Compile resolved authority and portable intent against one executor offer.
216
+ * The compiler never reads policy identifiers, host bindings, provider
217
+ * coordinates, runtime manifests, or implementation names.
218
+ */
219
+ function compileExecutionPlan(input) {
220
+ const { intent, offer } = input;
221
+ const stamp = {
222
+ mode: intent.mode,
223
+ phase: "preflight",
224
+ basis: "declared"
225
+ };
226
+ const decisions = [];
227
+ const deliverables = [];
228
+ const readinessByName = uniqueReadiness(input.credentialReadiness);
229
+ const offersByControl = groupOffers(offer.controls);
230
+ const authorized = intent.authority.authorizedControls;
231
+ const networkPatterns = [...intent.network.allowedHosts, ...intent.network.allowedInternalHosts];
232
+ let blocked = false;
233
+ for (const requirement of intent.credentialRequirements) {
234
+ const control = credentialAuthorityControl(requirement.name);
235
+ const fail = (reason, state) => {
236
+ const decisionState = state ?? (requirement.required ? "failed" : "degraded");
237
+ decisions.push({
238
+ control,
239
+ state: decisionState,
240
+ ...stamp,
241
+ reason
242
+ });
243
+ blocked ||= requirement.required;
244
+ };
245
+ if (authorized === void 0) {
246
+ fail("credential_authority_unresolved");
247
+ continue;
248
+ }
249
+ if (!authorized.includes(control)) {
250
+ fail("credential_authority_denied");
251
+ continue;
252
+ }
253
+ const readiness = readinessByName.get(requirement.name);
254
+ if (readiness === void 0) {
255
+ fail("credential_readiness_missing");
256
+ continue;
257
+ }
258
+ if (readiness.status !== "ready") {
259
+ fail(readiness.status);
260
+ continue;
261
+ }
262
+ if (requirement.destinations.some((destination) => !hostCovered(destination.host, networkPatterns))) {
263
+ fail("destination_not_in_network_intent");
264
+ continue;
265
+ }
266
+ if (requirement.lifecycle !== void 0 && requirement.lifecycle.maxTtlSec > intent.lease.ttlSec) {
267
+ fail("lifecycle_exceeds_lease");
268
+ continue;
269
+ }
270
+ const offerControl = credentialProjectionControl(requirement.projection);
271
+ const candidates = offersByControl.get(offerControl) ?? [];
272
+ const matchingCandidates = candidates.filter((candidate) => offerContainsRequirement(candidate, requirement));
273
+ if (matchingCandidates.length !== 1) {
274
+ fail(candidates.length === 0 ? "control_not_offered" : matchingCandidates.length === 0 ? "offer_constraints_mismatch" : "offer_ambiguous", "unsupported");
275
+ continue;
276
+ }
277
+ const candidate = matchingCandidates[0];
278
+ decisions.push({
279
+ control,
280
+ state: "enforced",
281
+ ...stamp,
282
+ offerControl,
283
+ enforcement: candidate.enforcement,
284
+ locus: candidate.locus
285
+ });
286
+ deliverables.push({
287
+ name: requirement.name,
288
+ projection: requirement.projection,
289
+ ...requirement.projection === "brokered-http" && { guestEnv: requirement.guestEnv },
290
+ required: requirement.required,
291
+ destinations: cloneDestinations(requirement.destinations),
292
+ offerControl,
293
+ enforcement: candidate.enforcement,
294
+ locus: candidate.locus
295
+ });
296
+ }
297
+ for (const name of intent.requiredCapabilities) {
298
+ const control = hostCapabilityControl(name);
299
+ if (authorized === void 0) {
300
+ decisions.push({
301
+ control,
302
+ state: "failed",
303
+ ...stamp,
304
+ reason: "capability_authority_unresolved"
305
+ });
306
+ blocked = true;
307
+ continue;
308
+ }
309
+ if (!authorized.includes(control)) {
310
+ decisions.push({
311
+ control,
312
+ state: "failed",
313
+ ...stamp,
314
+ reason: "capability_authority_denied"
315
+ });
316
+ blocked = true;
317
+ continue;
318
+ }
319
+ const candidates = offersByControl.get(control) ?? [];
320
+ if (candidates.length !== 1) {
321
+ decisions.push({
322
+ control,
323
+ state: "unsupported",
324
+ ...stamp,
325
+ reason: candidates.length === 0 ? "control_not_offered" : "offer_ambiguous"
326
+ });
327
+ blocked = true;
328
+ continue;
329
+ }
330
+ const candidate = candidates[0];
331
+ decisions.push({
332
+ control,
333
+ state: "enforced",
334
+ ...stamp,
335
+ offerControl: candidate.id,
336
+ enforcement: candidate.enforcement,
337
+ locus: candidate.locus
338
+ });
339
+ }
340
+ for (const control of intent.lease.requiredControls) {
341
+ const candidates = offersByControl.get(control) ?? [];
342
+ if (candidates.length !== 1) {
343
+ decisions.push({
344
+ control,
345
+ state: "unsupported",
346
+ ...stamp,
347
+ reason: candidates.length === 0 ? "control_not_offered" : "offer_ambiguous"
348
+ });
349
+ blocked = true;
350
+ continue;
351
+ }
352
+ const candidate = candidates[0];
353
+ decisions.push({
354
+ control,
355
+ state: "enforced",
356
+ ...stamp,
357
+ offerControl: candidate.id,
358
+ enforcement: candidate.enforcement,
359
+ locus: candidate.locus
360
+ });
361
+ }
362
+ return {
363
+ mode: intent.mode,
364
+ launchable: !blocked,
365
+ executor: { ...offer.executor },
366
+ decisions,
367
+ deliverables,
368
+ effectiveNetwork: {
369
+ allowedHosts: [...new Set(intent.network.allowedHosts)].sort(),
370
+ allowedInternalHosts: [...new Set(intent.network.allowedInternalHosts)].sort()
371
+ }
372
+ };
373
+ }
374
+ function groupOffers(controls) {
375
+ const grouped = /* @__PURE__ */ new Map();
376
+ for (const control of controls) {
377
+ const existing = grouped.get(control.id) ?? [];
378
+ existing.push(control);
379
+ grouped.set(control.id, existing);
380
+ }
381
+ return grouped;
382
+ }
383
+ function uniqueReadiness(readiness) {
384
+ const result = /* @__PURE__ */ new Map();
385
+ for (const record of readiness) {
386
+ if (result.has(record.name)) throw new Error(`duplicate credential readiness for "${record.name}"`);
387
+ result.set(record.name, record);
388
+ }
389
+ return result;
390
+ }
391
+ function offerContainsRequirement(offer, requirement) {
392
+ const offeredDestinations = offer.constraints?.destinations;
393
+ if (offeredDestinations === void 0 || !sameDestinationSet(offeredDestinations, requirement.destinations)) return false;
394
+ if (requirement.projection === "brokered-http") return offer.constraints?.guestEnvs?.includes(requirement.guestEnv) ?? false;
395
+ return true;
396
+ }
397
+ function sameDestinationSet(offered, requested) {
398
+ const offeredKeys = offered.map(destinationKey).sort();
399
+ const requestedKeys = requested.map(destinationKey).sort();
400
+ return offeredKeys.length === requestedKeys.length && offeredKeys.every((key, index) => key === requestedKeys[index]);
401
+ }
402
+ function destinationKey(destination) {
403
+ return `${destination.protocol}\0${destination.host}\0${destination.port}`;
404
+ }
405
+ function cloneDestinations(destinations) {
406
+ return destinations.map((destination) => ({ ...destination }));
407
+ }
408
+ /** Exact host, or a profile wildcard covering one or more subdomains. */
409
+ function hostCovered(host, patterns) {
410
+ return patterns.some((pattern) => {
411
+ if (pattern === host) return true;
412
+ if (!pattern.startsWith("*.")) return false;
413
+ const suffix = pattern.slice(1);
414
+ return host.endsWith(suffix) && host.length > suffix.length;
415
+ });
416
+ }
417
+ //#endregion
418
+ //#region ../../libs/execution-plan/src/credential-requirements.ts
419
+ var CredentialDestination = Type.Object({
420
+ protocol: Type.Union([Type.Literal("https"), Type.Literal("http")]),
421
+ host: Type.String({
422
+ minLength: 1,
423
+ maxLength: 255,
424
+ pattern: "^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*$"
425
+ }),
426
+ port: Type.Integer({
427
+ minimum: 1,
428
+ maximum: 65535
429
+ })
430
+ }, { additionalProperties: false });
431
+ var GuestEnvName = Type.String({
432
+ minLength: 1,
433
+ maxLength: 128,
434
+ pattern: "^(?!MOLTNET_)[A-Z][A-Z0-9_]*$"
435
+ });
436
+ var CredentialLifecycleIntent = Type.Object({
437
+ maxTtlSec: Type.Integer({
438
+ minimum: 1,
439
+ maximum: 86400
440
+ }),
441
+ refreshBeforeSec: Type.Integer({
442
+ minimum: 1,
443
+ maximum: 86400
444
+ })
445
+ }, { additionalProperties: false });
446
+ var credentialRequirementBase = {
447
+ name: Type.String({ minLength: 1 }),
448
+ kind: Type.Union([
449
+ Type.Literal("http-bearer"),
450
+ Type.Literal("http-basic"),
451
+ Type.Literal("api-key-header")
452
+ ]),
453
+ destinations: Type.Array(CredentialDestination, { minItems: 1 }),
454
+ required: Type.Boolean({ default: true }),
455
+ lifecycle: Type.Optional(CredentialLifecycleIntent)
456
+ };
457
+ var HostToolCredentialRequirement = Type.Object({
458
+ ...credentialRequirementBase,
459
+ projection: Type.Literal("host-tool")
460
+ }, { additionalProperties: false });
461
+ var BrokeredHttpCredentialRequirement = Type.Object({
462
+ ...credentialRequirementBase,
463
+ projection: Type.Literal("brokered-http"),
464
+ guestEnv: GuestEnvName
465
+ }, { additionalProperties: false });
466
+ var CredentialRequirement = Type.Union([HostToolCredentialRequirement, BrokeredHttpCredentialRequirement]);
467
+ function parseCredentialRequirements(input) {
468
+ const withDefaults = Value.Default(Type.Array(CredentialRequirement), Value.Clone(input));
469
+ const requirements = Value.Parse(Type.Array(CredentialRequirement), withDefaults);
470
+ const names = /* @__PURE__ */ new Set();
471
+ const guestEnvs = /* @__PURE__ */ new Set();
472
+ for (const requirement of requirements) {
473
+ if (names.has(requirement.name)) throw new Error(`duplicate credential requirement name "${requirement.name}"`);
474
+ names.add(requirement.name);
475
+ if (requirement.projection === "brokered-http") {
476
+ if (guestEnvs.has(requirement.guestEnv)) throw new Error(`duplicate brokered guestEnv "${requirement.guestEnv}"`);
477
+ guestEnvs.add(requirement.guestEnv);
478
+ }
479
+ const lifecycle = requirement.lifecycle;
480
+ if (lifecycle && lifecycle.refreshBeforeSec >= lifecycle.maxTtlSec) throw new Error(`credential requirement "${requirement.name}": refreshBeforeSec must be shorter than maxTtlSec`);
481
+ }
482
+ return requirements;
483
+ }
484
+ //#endregion
485
+ //#region ../../libs/execution-plan/src/execution-snapshot.ts
486
+ /**
487
+ * Create an immutable, content-addressed, value-free execution snapshot. The
488
+ * existing policy snapshot hash is pinned inside intent; policy composition
489
+ * has already happened before this boundary.
490
+ */
491
+ async function createExecutionPlanSnapshot(input) {
492
+ const body = structuredClone(input);
493
+ return deepFreeze({
494
+ cid: await computeJsonCid(snapshotBody(body)),
495
+ ...body
496
+ });
497
+ }
498
+ function snapshotBody(input) {
499
+ return {
500
+ v: "moltnet:execution-plan-snapshot:v1",
501
+ ...input
502
+ };
503
+ }
504
+ function deepFreeze(value) {
505
+ if (value !== null && typeof value === "object") {
506
+ for (const child of Object.values(value)) deepFreeze(child);
507
+ Object.freeze(value);
508
+ }
509
+ return value;
510
+ }
511
+ //#endregion
512
+ //#region src/lib/runtime-governance.ts
513
+ var executionOffers = /* @__PURE__ */ new WeakMap();
514
+ /** Attach private governance metadata without widening the public adapter API. */
515
+ function registerRuntimeExecutionOffer(runtime, factory) {
516
+ executionOffers.set(runtime, factory);
517
+ }
518
+ /** Resolve the selected runtime's offer for the observe-only orchestrator. */
519
+ function runtimeExecutionOffer(runtime, executorFingerprint) {
520
+ return executionOffers.get(runtime)?.(executorFingerprint);
521
+ }
522
+ //#endregion
523
+ //#region ../../libs/execution-integrations/src/pi.ts
524
+ /** Convert the canonical Pi manifest to a portable, open-ended offer. */
525
+ function executionCapabilityOfferFromPiManifest(manifest, options) {
526
+ const enforcement = options.enforcement ?? "native";
527
+ const locus = options.locus ?? "pi:isolated-runtime";
528
+ return {
529
+ executor: {
530
+ id: `${manifest.runtime.id}@${manifest.runtime.version}`,
531
+ fingerprint: options.executorFingerprint
532
+ },
533
+ controls: [...(manifest.brokeredHttpSecrets ?? []).map((secret) => ({
534
+ id: credentialProjectionControl("brokered-http"),
535
+ enforcement,
536
+ locus,
537
+ constraints: {
538
+ destinations: manifestDestinations(secret),
539
+ guestEnvs: [secret.guestEnv]
540
+ }
541
+ })), ...(manifest.hostCapabilities ?? []).map((capability) => ({
542
+ id: hostCapabilityControl(capability.name),
543
+ enforcement,
544
+ locus
545
+ }))]
546
+ };
547
+ }
548
+ function manifestDestinations(secret) {
549
+ return secret.hosts.flatMap((host) => secret.ports.map((port) => ({
550
+ protocol: secret.protocol,
551
+ host,
552
+ port
553
+ })));
554
+ }
555
+ //#endregion
3
556
  //#region src/pi.ts
4
557
  var PI_KERNEL_TOOL_NAMES = [
5
558
  ...GONDOLIN_TOOL_NAMES,
@@ -29,7 +582,7 @@ function createPiDaemonAdapter(runtime) {
29
582
  builtInToolNames: PI_KERNEL_TOOL_NAMES
30
583
  });
31
584
  const extensionTools = runtime.extensions.flatMap((extension) => extension.declaredTools);
32
- return {
585
+ const prepared = {
33
586
  runtimeKind: runtime.runtimeKind,
34
587
  manifest,
35
588
  tools: [
@@ -49,6 +602,8 @@ function createPiDaemonAdapter(runtime) {
49
602
  resolvedVmTemplate: resolvedTemplate
50
603
  })
51
604
  };
605
+ registerRuntimeExecutionOffer(prepared, (executorFingerprint) => executionCapabilityOfferFromPiManifest(manifest, { executorFingerprint }));
606
+ return prepared;
52
607
  }
53
608
  };
54
609
  }
@@ -64,4 +619,4 @@ var defaultPiRuntimeDefinition = definePiRuntime({
64
619
  });
65
620
  var defaultPiDaemonAdapter = createPiDaemonAdapter(defaultPiRuntimeDefinition);
66
621
  //#endregion
67
- export { PI_KERNEL_TOOL_NAMES, createPiDaemonAdapter, defaultPiDaemonAdapter, defaultPiRuntimeDefinition };
622
+ export { PI_KERNEL_TOOL_NAMES, cryptoService as a, createPiDaemonAdapter, defaultPiDaemonAdapter, defaultPiRuntimeDefinition, compileExecutionPlan as i, createExecutionPlanSnapshot as n, parseCredentialRequirements as r, runtimeExecutionOffer as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.45.0",
3
+ "version": "0.46.0",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "description": "Universal MoltNet agent daemon host with a built-in Pi/Gondolin runtime and support for trusted operator-owned runtime modules. CLI: moltnet-agent.",
@@ -56,7 +56,7 @@
56
56
  "@noble/curves": "^2.0.0",
57
57
  "@noble/ed25519": "^2.0.0",
58
58
  "@noble/hashes": "^1.7.0",
59
- "@opentelemetry/api": "^1.9.0",
59
+ "@opentelemetry/api": "^1.9.1",
60
60
  "@opentelemetry/exporter-metrics-otlp-proto": "^0.212.0",
61
61
  "@opentelemetry/exporter-trace-otlp-proto": "^0.212.0",
62
62
  "@opentelemetry/instrumentation": "^0.212.0",
@@ -78,10 +78,10 @@
78
78
  "pino-opentelemetry-transport": "^3.0.0",
79
79
  "pino-pretty": "^13.1.3",
80
80
  "typebox": "^1.2.8",
81
+ "@themoltnet/agent-runtime": "0.45.1",
81
82
  "@themoltnet/os-keyring": "0.3.0",
82
- "@themoltnet/agent-runtime": "0.45.0",
83
- "@themoltnet/pi-runtime": "0.12.2",
84
- "@themoltnet/sdk": "0.138.0"
83
+ "@themoltnet/pi-runtime": "0.13.0",
84
+ "@themoltnet/sdk": "0.139.0"
85
85
  },
86
86
  "devDependencies": {
87
87
  "tsx": "^4.7.0",
@@ -91,10 +91,12 @@
91
91
  "vitest": "^3.0.0",
92
92
  "@moltnet/bootstrap": "0.1.0",
93
93
  "@moltnet/crypto-service": "0.1.0",
94
+ "@moltnet/execution-integrations": "0.1.0",
94
95
  "@moltnet/models": "0.1.0",
96
+ "@moltnet/execution-plan": "0.1.0",
95
97
  "@moltnet/observability": "0.1.0",
96
- "@moltnet/runtime-profiles": "0.1.0",
97
- "@moltnet/tasks": "0.1.0"
98
+ "@moltnet/tasks": "0.1.0",
99
+ "@moltnet/runtime-profiles": "0.1.0"
98
100
  },
99
101
  "nx": {
100
102
  "projectType": "application",