@haaaiawd/second-nature 0.1.8 → 0.1.10

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.
Files changed (185) hide show
  1. package/index.js +281 -69
  2. package/openclaw.plugin.json +1 -1
  3. package/package.json +2 -1
  4. package/runtime/cli/commands/index.d.ts +2 -0
  5. package/runtime/cli/commands/index.js +61 -6
  6. package/runtime/cli/explain/explain-surface-subject.d.ts +8 -0
  7. package/runtime/cli/explain/explain-surface-subject.js +9 -0
  8. package/runtime/cli/explain/format-explanation.d.ts +2 -0
  9. package/runtime/cli/explain/format-explanation.js +2 -0
  10. package/runtime/cli/explain/resolve-subject.js +15 -0
  11. package/runtime/cli/host-capability/classify-delivery.d.ts +14 -0
  12. package/runtime/cli/host-capability/classify-delivery.js +20 -0
  13. package/runtime/cli/host-capability/probe-host-capability.d.ts +2 -0
  14. package/runtime/cli/host-capability/probe-host-capability.js +58 -0
  15. package/runtime/cli/host-capability/record-host-capability.d.ts +6 -0
  16. package/runtime/cli/host-capability/record-host-capability.js +14 -0
  17. package/runtime/cli/host-capability/types.d.ts +71 -0
  18. package/runtime/cli/host-capability/types.js +6 -0
  19. package/runtime/cli/host-smoke/run-host-smoke.d.ts +2 -0
  20. package/runtime/cli/host-smoke/run-host-smoke.js +40 -0
  21. package/runtime/cli/host-smoke/types.d.ts +35 -0
  22. package/runtime/cli/host-smoke/types.js +6 -0
  23. package/runtime/cli/index.js +18 -0
  24. package/runtime/cli/ops/heartbeat-surface.d.ts +35 -0
  25. package/runtime/cli/ops/heartbeat-surface.js +71 -0
  26. package/runtime/cli/ops/ops-router.d.ts +16 -0
  27. package/runtime/cli/ops/ops-router.js +83 -0
  28. package/runtime/cli/ops/show-operator-fallback.d.ts +13 -0
  29. package/runtime/cli/ops/show-operator-fallback.js +22 -0
  30. package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +10 -0
  31. package/runtime/cli/ops/workspace-heartbeat-runner.js +26 -0
  32. package/runtime/cli/read-models/index.d.ts +11 -2
  33. package/runtime/cli/read-models/index.js +50 -0
  34. package/runtime/cli/read-models/operator-explain-map.d.ts +6 -0
  35. package/runtime/cli/read-models/operator-explain-map.js +10 -0
  36. package/runtime/cli/read-models/types.d.ts +5 -1
  37. package/runtime/cli/runtime/runtime-artifact-boundary.d.ts +28 -0
  38. package/runtime/cli/runtime/runtime-artifact-boundary.js +94 -0
  39. package/runtime/connectors/base/contract.d.ts +6 -0
  40. package/runtime/connectors/base/execution-policy.d.ts +47 -0
  41. package/runtime/connectors/base/execution-policy.js +82 -0
  42. package/runtime/connectors/base/index.d.ts +2 -0
  43. package/runtime/connectors/base/index.js +2 -0
  44. package/runtime/connectors/base/manifest.d.ts +55 -2
  45. package/runtime/connectors/base/manifest.js +50 -0
  46. package/runtime/connectors/base/map-life-evidence.d.ts +16 -0
  47. package/runtime/connectors/base/map-life-evidence.js +79 -0
  48. package/runtime/connectors/base/policy-layer.d.ts +2 -0
  49. package/runtime/connectors/base/policy-layer.js +16 -0
  50. package/runtime/connectors/base/route-planner.js +1 -0
  51. package/runtime/connectors/index.d.ts +1 -0
  52. package/runtime/connectors/index.js +1 -0
  53. package/runtime/connectors/near-real/near-real-connector-smoke.d.ts +19 -0
  54. package/runtime/connectors/near-real/near-real-connector-smoke.js +152 -0
  55. package/runtime/core/second-nature/heartbeat/heartbeat-executor.js +2 -0
  56. package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +37 -16
  57. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +95 -29
  58. package/runtime/core/second-nature/heartbeat/index.d.ts +4 -1
  59. package/runtime/core/second-nature/heartbeat/index.js +4 -1
  60. package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle.d.ts +21 -0
  61. package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle.js +35 -0
  62. package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +28 -0
  63. package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +35 -0
  64. package/runtime/core/second-nature/heartbeat/signal.d.ts +9 -2
  65. package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +19 -1
  66. package/runtime/core/second-nature/index.d.ts +8 -0
  67. package/runtime/core/second-nature/index.js +8 -0
  68. package/runtime/core/second-nature/orchestrator/effect-dispatcher.d.ts +1 -1
  69. package/runtime/core/second-nature/orchestrator/effect-dispatcher.js +9 -4
  70. package/runtime/core/second-nature/orchestrator/guard-layer.d.ts +6 -0
  71. package/runtime/core/second-nature/orchestrator/guard-layer.js +76 -20
  72. package/runtime/core/second-nature/orchestrator/intent-planner.d.ts +10 -0
  73. package/runtime/core/second-nature/orchestrator/intent-planner.js +135 -28
  74. package/runtime/core/second-nature/orchestrator/lease-manager.d.ts +1 -1
  75. package/runtime/core/second-nature/orchestrator/lease-manager.js +1 -1
  76. package/runtime/core/second-nature/outreach/build-outreach-draft-request.d.ts +6 -0
  77. package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +63 -0
  78. package/runtime/core/second-nature/outreach/delivery-target.d.ts +26 -0
  79. package/runtime/core/second-nature/outreach/delivery-target.js +70 -0
  80. package/runtime/core/second-nature/outreach/dispatch-user-outreach.d.ts +38 -0
  81. package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +119 -0
  82. package/runtime/core/second-nature/outreach/judge-input-from-snapshot.d.ts +7 -0
  83. package/runtime/core/second-nature/outreach/judge-input-from-snapshot.js +45 -0
  84. package/runtime/core/second-nature/outreach/judge-outreach.d.ts +40 -0
  85. package/runtime/core/second-nature/outreach/judge-outreach.js +121 -0
  86. package/runtime/core/second-nature/quiet/run-source-backed-quiet.d.ts +21 -0
  87. package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +123 -0
  88. package/runtime/core/second-nature/rhythm/planner-rhythm-window.d.ts +15 -0
  89. package/runtime/core/second-nature/rhythm/planner-rhythm-window.js +52 -0
  90. package/runtime/core/second-nature/rhythm/policy-bridge.d.ts +19 -0
  91. package/runtime/core/second-nature/rhythm/policy-bridge.js +34 -0
  92. package/runtime/core/second-nature/types.d.ts +16 -2
  93. package/runtime/guidance/draft-outreach-message.d.ts +7 -0
  94. package/runtime/guidance/draft-outreach-message.js +42 -0
  95. package/runtime/guidance/evidence-guidance.d.ts +40 -0
  96. package/runtime/guidance/evidence-guidance.js +52 -0
  97. package/runtime/guidance/index.d.ts +3 -0
  98. package/runtime/guidance/index.js +3 -0
  99. package/runtime/guidance/outreach-draft-schema.d.ts +228 -0
  100. package/runtime/guidance/outreach-draft-schema.js +80 -0
  101. package/runtime/observability/audit/append-only-audit-store.d.ts +14 -0
  102. package/runtime/observability/audit/append-only-audit-store.js +21 -0
  103. package/runtime/observability/audit/audit-envelope.d.ts +51 -0
  104. package/runtime/observability/audit/audit-envelope.js +130 -0
  105. package/runtime/observability/audit/verify-audit-hash-chain.d.ts +23 -0
  106. package/runtime/observability/audit/verify-audit-hash-chain.js +83 -0
  107. package/runtime/observability/db/index.js +11 -0
  108. package/runtime/observability/db/schema/host-capability-reports.d.ts +180 -0
  109. package/runtime/observability/db/schema/host-capability-reports.js +12 -0
  110. package/runtime/observability/db/schema/index.d.ts +1 -0
  111. package/runtime/observability/db/schema/index.js +1 -0
  112. package/runtime/observability/index.d.ts +7 -0
  113. package/runtime/observability/index.js +7 -0
  114. package/runtime/observability/query/explain-query.d.ts +48 -0
  115. package/runtime/observability/query/explain-query.js +114 -0
  116. package/runtime/observability/query/export-audit-bundle.d.ts +22 -0
  117. package/runtime/observability/query/export-audit-bundle.js +27 -0
  118. package/runtime/observability/services/decision-ledger.d.ts +1 -1
  119. package/runtime/observability/services/decision-ledger.js +4 -0
  120. package/runtime/observability/services/governance-audit.d.ts +14 -0
  121. package/runtime/observability/services/governance-audit.js +25 -1
  122. package/runtime/observability/services/governance-plane-recorder.d.ts +47 -0
  123. package/runtime/observability/services/governance-plane-recorder.js +55 -0
  124. package/runtime/observability/services/lived-experience-audit.d.ts +97 -0
  125. package/runtime/observability/services/lived-experience-audit.js +162 -0
  126. package/runtime/storage/bootstrap/native-sqlite-probe.d.ts +7 -0
  127. package/runtime/storage/bootstrap/native-sqlite-probe.js +28 -0
  128. package/runtime/storage/bootstrap/repair-gate.d.ts +17 -0
  129. package/runtime/storage/bootstrap/repair-gate.js +71 -0
  130. package/runtime/storage/bootstrap/storage-mode-smoke.d.ts +38 -0
  131. package/runtime/storage/bootstrap/storage-mode-smoke.js +85 -0
  132. package/runtime/storage/db/index.js +49 -0
  133. package/runtime/storage/db/schema/delivery-attempts.d.ts +199 -0
  134. package/runtime/storage/db/schema/delivery-attempts.js +13 -0
  135. package/runtime/storage/db/schema/index.d.ts +3 -0
  136. package/runtime/storage/db/schema/index.js +3 -0
  137. package/runtime/storage/db/schema/life-evidence-index.d.ts +161 -0
  138. package/runtime/storage/db/schema/life-evidence-index.js +11 -0
  139. package/runtime/storage/db/schema/operator-fallback-artifacts.d.ts +161 -0
  140. package/runtime/storage/db/schema/operator-fallback-artifacts.js +11 -0
  141. package/runtime/storage/db/schema/policies.d.ts +17 -0
  142. package/runtime/storage/db/schema/policies.js +1 -0
  143. package/runtime/storage/delivery/query-delivery-attempts.d.ts +3 -0
  144. package/runtime/storage/delivery/query-delivery-attempts.js +32 -0
  145. package/runtime/storage/delivery/types.d.ts +27 -0
  146. package/runtime/storage/delivery/types.js +1 -0
  147. package/runtime/storage/delivery/write-delivery-attempt.d.ts +6 -0
  148. package/runtime/storage/delivery/write-delivery-attempt.js +36 -0
  149. package/runtime/storage/fallback/load-operator-fallback.d.ts +14 -0
  150. package/runtime/storage/fallback/load-operator-fallback.js +47 -0
  151. package/runtime/storage/fallback/operator-fallback-types.d.ts +9 -0
  152. package/runtime/storage/fallback/operator-fallback-types.js +1 -0
  153. package/runtime/storage/fallback/operator-fallback-view.d.ts +11 -0
  154. package/runtime/storage/fallback/operator-fallback-view.js +1 -0
  155. package/runtime/storage/fallback/write-operator-fallback.d.ts +6 -0
  156. package/runtime/storage/fallback/write-operator-fallback.js +21 -0
  157. package/runtime/storage/index.d.ts +21 -0
  158. package/runtime/storage/index.js +14 -0
  159. package/runtime/storage/life-evidence/append-life-evidence.d.ts +7 -0
  160. package/runtime/storage/life-evidence/append-life-evidence.js +64 -0
  161. package/runtime/storage/life-evidence/types.d.ts +45 -0
  162. package/runtime/storage/life-evidence/types.js +6 -0
  163. package/runtime/storage/quiet/persist-quiet-artifact.d.ts +7 -0
  164. package/runtime/storage/quiet/persist-quiet-artifact.js +22 -0
  165. package/runtime/storage/quiet/quiet-artifact-types.d.ts +18 -0
  166. package/runtime/storage/quiet/quiet-artifact-types.js +1 -0
  167. package/runtime/storage/quiet/quiet-artifact-writer.d.ts +15 -0
  168. package/runtime/storage/quiet/quiet-artifact-writer.js +56 -0
  169. package/runtime/storage/repositories/credential-repository.js +12 -1
  170. package/runtime/storage/rhythm/rhythm-policy-snapshot.d.ts +10 -0
  171. package/runtime/storage/rhythm/rhythm-policy-snapshot.js +34 -0
  172. package/runtime/storage/services/credential-vault.d.ts +5 -0
  173. package/runtime/storage/services/credential-vault.js +47 -9
  174. package/runtime/storage/snapshots/continuity-snapshot.d.ts +9 -0
  175. package/runtime/storage/snapshots/continuity-snapshot.js +41 -0
  176. package/runtime/storage/snapshots/life-evidence-snapshot.d.ts +6 -0
  177. package/runtime/storage/snapshots/life-evidence-snapshot.js +114 -0
  178. package/runtime/storage/snapshots/types.d.ts +58 -0
  179. package/runtime/storage/snapshots/types.js +1 -0
  180. package/runtime/storage/state-api.js +11 -4
  181. package/runtime/storage/user-interest/load-user-interest-snapshot.d.ts +2 -0
  182. package/runtime/storage/user-interest/load-user-interest-snapshot.js +150 -0
  183. package/runtime/storage/user-interest/types.d.ts +25 -0
  184. package/runtime/storage/user-interest/types.js +1 -0
  185. package/workspace-ops-bridge.js +78 -0
@@ -0,0 +1,18 @@
1
+ import type { SourceRef } from "../life-evidence/types.js";
2
+ export type QuietArtifactKind = "daily_report" | "narrative_reflection" | "curated_memory_candidate" | "empty_state";
3
+ export type QuietClaimType = "fact" | "emotion" | "interpretation" | "next_step";
4
+ export interface QuietClaim {
5
+ id: string;
6
+ text: string;
7
+ sourceRefs: SourceRef[];
8
+ claimType: QuietClaimType;
9
+ }
10
+ export interface QuietArtifactWrite {
11
+ day: string;
12
+ kind: QuietArtifactKind;
13
+ title: string;
14
+ body: string;
15
+ claims: QuietClaim[];
16
+ sourceRefs: SourceRef[];
17
+ memoryCandidateRefs?: SourceRef[];
18
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { SourceRef } from "../life-evidence/types.js";
2
+ import type { SourceCoverage } from "../snapshots/types.js";
3
+ import type { QuietArtifactWrite } from "./quiet-artifact-types.js";
4
+ import type { QuietClaim } from "./quiet-artifact-types.js";
5
+ export declare function calculateQuietSourceCoverage(claims: QuietClaim[]): SourceCoverage;
6
+ /** Ratio of factual claims whose refs intersect the artifact's evidence bundle (T4.4.1). */
7
+ export declare function evidenceGroundingRatio(input: Pick<QuietArtifactWrite, "claims" | "sourceRefs">): number;
8
+ export interface QuietArtifactAck {
9
+ artifactId: string;
10
+ artifactRef: SourceRef;
11
+ sourceCoverage: SourceCoverage;
12
+ }
13
+ export declare function writeQuietArtifact(input: QuietArtifactWrite, opts?: {
14
+ minimumCoverageRatio?: number;
15
+ }): QuietArtifactAck;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Quiet artifact validation + source coverage gate (T4.4.1 / ADR-003).
3
+ * Returns artifact identity for persistence layers; does not write FS/SQLite here.
4
+ */
5
+ import * as crypto from "node:crypto";
6
+ const DEFAULT_MIN_COVERAGE_RATIO = 0.51;
7
+ export function calculateQuietSourceCoverage(claims) {
8
+ const factual = claims.filter((c) => c.claimType === "fact");
9
+ const claimCoverage = claims.map((c) => {
10
+ const backed = c.claimType !== "fact" || c.sourceRefs.length > 0;
11
+ return { claimId: c.id, backed, sourceRefs: [...c.sourceRefs] };
12
+ });
13
+ const unsupportedClaims = claimCoverage.filter((x) => !x.backed).map((x) => x.claimId);
14
+ const coverageRatio = factual.length === 0 ? 1 : factual.filter((c) => c.sourceRefs.length > 0).length / factual.length;
15
+ return { coverageRatio, unsupportedClaims, claimCoverage };
16
+ }
17
+ /** Ratio of factual claims whose refs intersect the artifact's evidence bundle (T4.4.1). */
18
+ export function evidenceGroundingRatio(input) {
19
+ const allowedIds = new Set(input.sourceRefs.map((s) => s.id));
20
+ const facts = input.claims.filter((c) => c.claimType === "fact");
21
+ if (facts.length === 0)
22
+ return 1;
23
+ let grounded = 0;
24
+ for (const f of facts) {
25
+ if (f.sourceRefs.length === 0)
26
+ continue;
27
+ if (f.sourceRefs.some((r) => allowedIds.has(r.id)))
28
+ grounded += 1;
29
+ }
30
+ return grounded / facts.length;
31
+ }
32
+ export function writeQuietArtifact(input, opts) {
33
+ const minCov = opts?.minimumCoverageRatio ?? DEFAULT_MIN_COVERAGE_RATIO;
34
+ if (input.sourceRefs.length === 0 && input.kind !== "empty_state") {
35
+ throw new Error("quiet_artifact_requires_source_refs");
36
+ }
37
+ const localCoverage = calculateQuietSourceCoverage(input.claims);
38
+ if (input.kind !== "empty_state" && localCoverage.unsupportedClaims.length > 0) {
39
+ throw new Error("quiet_artifact_unsupported_factual_claim");
40
+ }
41
+ const grounding = evidenceGroundingRatio(input);
42
+ if (input.kind !== "empty_state" && grounding < minCov) {
43
+ throw new Error("quiet_artifact_source_coverage_too_low");
44
+ }
45
+ const artifactId = crypto.randomUUID();
46
+ const artifactRef = {
47
+ id: `quiet:${artifactId}`,
48
+ kind: "workspace_artifact",
49
+ uri: `urn:second-nature:quiet:${input.day}:${input.kind}:${artifactId}`,
50
+ };
51
+ const sourceCoverage = {
52
+ ...localCoverage,
53
+ coverageRatio: input.kind === "empty_state" ? 1 : grounding,
54
+ };
55
+ return { artifactId, artifactRef, sourceCoverage };
56
+ }
@@ -12,8 +12,19 @@ export class CredentialRepository {
12
12
  });
13
13
  }
14
14
  async findByPlatformId(platformId) {
15
- return this.database.db.query.credentialRecords.findFirst({
15
+ const row = await this.database.db.query.credentialRecords.findFirst({
16
16
  where: eq(credentialRecords.platformId, platformId),
17
17
  });
18
+ if (row == null) {
19
+ return undefined;
20
+ }
21
+ const r = row;
22
+ const pid = (r.platformId ?? r.platform_id);
23
+ const enc = (r.encryptedValue ?? r.encrypted_value);
24
+ // sql.js + Drizzle: no-match can still return a "shell" row (keys present, values undefined).
25
+ if (pid == null || pid === "" || enc == null || enc === "") {
26
+ return undefined;
27
+ }
28
+ return row;
18
29
  }
19
30
  }
@@ -0,0 +1,10 @@
1
+ import type { StateDatabase } from "../db/index.js";
2
+ export interface RhythmPolicySnapshot {
3
+ snapshotId: string;
4
+ generatedAt: string;
5
+ quietEnabled: boolean;
6
+ socialDailyLimit: number;
7
+ outreachDailyBudget: number;
8
+ updatedAt: string;
9
+ }
10
+ export declare function loadRhythmPolicySnapshot(db: StateDatabase): Promise<RhythmPolicySnapshot>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Rhythm policy read model: state produces policy fields only (T4.1.2 owner boundary).
3
+ *
4
+ * Core logic: load workspace-scoped policy row or defaults; never emit control-plane decision fields.
5
+ *
6
+ * Test coverage: tests/unit/storage/rhythm-policy-snapshot.test.ts
7
+ */
8
+ import * as crypto from "node:crypto";
9
+ import { eq } from "drizzle-orm";
10
+ import { policyRecords } from "../db/schema/policies.js";
11
+ const WORKSPACE_SCOPE = "workspace";
12
+ export async function loadRhythmPolicySnapshot(db) {
13
+ const scoped = await db.db.select().from(policyRecords).where(eq(policyRecords.platformId, WORKSPACE_SCOPE)).limit(1);
14
+ const row = scoped[0] ?? (await db.db.select().from(policyRecords).limit(1))[0];
15
+ const generatedAt = new Date().toISOString();
16
+ if (!row) {
17
+ return {
18
+ snapshotId: crypto.randomUUID(),
19
+ generatedAt,
20
+ quietEnabled: true,
21
+ socialDailyLimit: 5,
22
+ outreachDailyBudget: 2,
23
+ updatedAt: generatedAt,
24
+ };
25
+ }
26
+ return {
27
+ snapshotId: crypto.randomUUID(),
28
+ generatedAt,
29
+ quietEnabled: row.quietEnabled,
30
+ socialDailyLimit: row.socialDailyLimit,
31
+ outreachDailyBudget: row.outreachDailyBudget ?? 2,
32
+ updatedAt: row.updatedAt,
33
+ };
34
+ }
@@ -1,5 +1,10 @@
1
1
  import type { StateDatabase } from "../db/index.js";
2
2
  import type { CredentialContextWrite, CredentialContext, CredentialState } from "../../shared/types/index.js";
3
+ /** Three colon-separated hex segments produced by `encryptCredentialAtRest`. */
4
+ export declare function isCredentialCiphertext(value: string): boolean;
5
+ /** Encrypts non-empty plaintext; empty string returns empty. */
6
+ export declare function encryptCredentialAtRest(plaintext: string): string;
7
+ export declare function decryptCredentialAtRest(ciphertext: string): string;
3
8
  export interface CredentialVault {
4
9
  saveCredentialContext(input: CredentialContextWrite): Promise<void>;
5
10
  loadCredentialContext(platformId: string): Promise<CredentialContext | null>;
@@ -1,32 +1,63 @@
1
+ /**
2
+ * Credential encryption at rest (AES-256-GCM).
3
+ *
4
+ * Key material: `SECOND_NATURE_ENCRYPTION_KEY` (UTF-8, first 32 bytes used). Lazy read so tests can set env before first encrypt.
5
+ * Test coverage: tests/integration/cli/cli-ops-surface.test.ts (credential save path via state-api).
6
+ */
1
7
  import * as crypto from "crypto";
2
8
  import { eq } from "drizzle-orm";
3
9
  import { credentialRecords } from "../db/schema/index.js";
4
- const ENCRYPTION_KEY = process.env.SECOND_NATURE_ENCRYPTION_KEY || crypto.randomBytes(32).toString("hex");
5
10
  const ALGORITHM = "aes-256-gcm";
6
- function encrypt(plaintext) {
11
+ function resolveKeyBuffer() {
12
+ const raw = process.env.SECOND_NATURE_ENCRYPTION_KEY?.trim();
13
+ if (!raw || raw.length < 32) {
14
+ throw new Error("SECOND_NATURE_ENCRYPTION_KEY is required for credential encryption at rest (min 32 UTF-8 characters)");
15
+ }
16
+ return Buffer.from(raw.slice(0, 32), "utf8");
17
+ }
18
+ function encryptInternal(plaintext) {
7
19
  const iv = crypto.randomBytes(16);
8
- const key = Buffer.from(ENCRYPTION_KEY.slice(0, 32), "utf8");
20
+ const key = resolveKeyBuffer();
9
21
  const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
10
22
  const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
11
23
  const authTag = cipher.getAuthTag();
12
24
  return iv.toString("hex") + ":" + authTag.toString("hex") + ":" + encrypted.toString("hex");
13
25
  }
14
- function decrypt(ciphertext) {
26
+ function decryptInternal(ciphertext) {
15
27
  const parts = ciphertext.split(":");
16
- if (parts.length !== 3)
17
- return ciphertext;
28
+ if (parts.length !== 3) {
29
+ throw new Error("credential_ciphertext_invalid_format");
30
+ }
18
31
  const iv = Buffer.from(parts[0], "hex");
19
32
  const authTag = Buffer.from(parts[1], "hex");
20
33
  const encrypted = Buffer.from(parts[2], "hex");
21
- const key = Buffer.from(ENCRYPTION_KEY.slice(0, 32), "utf8");
34
+ const key = resolveKeyBuffer();
22
35
  const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
23
36
  decipher.setAuthTag(authTag);
24
37
  return decipher.update(encrypted) + decipher.final("utf8");
25
38
  }
39
+ /** Three colon-separated hex segments produced by `encryptCredentialAtRest`. */
40
+ export function isCredentialCiphertext(value) {
41
+ const parts = value.split(":");
42
+ if (parts.length !== 3)
43
+ return false;
44
+ return parts.every((p) => /^[0-9a-f]+$/i.test(p));
45
+ }
46
+ /** Encrypts non-empty plaintext; empty string returns empty. */
47
+ export function encryptCredentialAtRest(plaintext) {
48
+ if (!plaintext)
49
+ return "";
50
+ return encryptInternal(plaintext);
51
+ }
52
+ export function decryptCredentialAtRest(ciphertext) {
53
+ if (!ciphertext)
54
+ return "";
55
+ return decryptInternal(ciphertext);
56
+ }
26
57
  export function createCredentialVault(db) {
27
58
  return {
28
59
  async saveCredentialContext(input) {
29
- const encrypted = input.encryptedValue ? encrypt(input.encryptedValue) : "";
60
+ const encrypted = input.encryptedValue ? encryptCredentialAtRest(input.encryptedValue) : "";
30
61
  await db.insert(credentialRecords).values({
31
62
  platformId: input.platformId,
32
63
  credentialType: input.credentialType,
@@ -57,11 +88,18 @@ export function createCredentialVault(db) {
57
88
  });
58
89
  if (!record)
59
90
  return null;
91
+ let plain;
92
+ if (record.encryptedValue) {
93
+ if (!isCredentialCiphertext(record.encryptedValue)) {
94
+ throw new Error("credential_store_plaintext_or_invalid_legacy_record");
95
+ }
96
+ plain = decryptCredentialAtRest(record.encryptedValue);
97
+ }
60
98
  return {
61
99
  platformId: record.platformId,
62
100
  credentialType: record.credentialType,
63
101
  status: record.status,
64
- encryptedValue: record.encryptedValue ? decrypt(record.encryptedValue) : undefined,
102
+ encryptedValue: plain,
65
103
  verificationCode: record.verificationCode ?? undefined,
66
104
  challengeText: record.challengeText ?? undefined,
67
105
  verificationDeadline: record.expiresAt ?? undefined,
@@ -0,0 +1,9 @@
1
+ import type { ObservabilityDatabase } from "../../observability/db/index.js";
2
+ import type { StateDatabase } from "../db/index.js";
3
+ import type { ContinuitySnapshot } from "./types.js";
4
+ export interface LoadContinuitySnapshotParams {
5
+ state: StateDatabase;
6
+ workspaceRoot: string;
7
+ observability?: ObservabilityDatabase;
8
+ }
9
+ export declare function loadContinuitySnapshot(params: LoadContinuitySnapshotParams): Promise<ContinuitySnapshot>;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Continuity snapshot read model (T4.2.1) — lightweight bridge until full state feeds land.
3
+ */
4
+ import * as crypto from "node:crypto";
5
+ import { desc } from "drizzle-orm";
6
+ import { decisionLedger } from "../../observability/db/schema/index.js";
7
+ import { repairStateIndexes } from "../bootstrap/repair-gate.js";
8
+ import { loadLifeEvidenceSnapshot } from "./life-evidence-snapshot.js";
9
+ export async function loadContinuitySnapshot(params) {
10
+ await repairStateIndexes(params.state, { startupGate: true, workspaceRoot: params.workspaceRoot });
11
+ const life = await loadLifeEvidenceSnapshot(params.state, params.workspaceRoot, { limit: 50 }, { runRepairGate: false });
12
+ let recentDecisionRefs = [];
13
+ if (params.observability) {
14
+ const rows = await params.observability.db
15
+ .select()
16
+ .from(decisionLedger)
17
+ .orderBy(desc(decisionLedger.createdAt))
18
+ .limit(5);
19
+ recentDecisionRefs = rows.map((r) => ({
20
+ id: r.id,
21
+ kind: "decision_record",
22
+ uri: `sn://decision/${r.id}`,
23
+ }));
24
+ }
25
+ const pendingCount = life.platformEvents.length + life.workEvents.length + life.userInteractionEvents.length;
26
+ const oldest = [...life.platformEvents, ...life.workEvents, ...life.userInteractionEvents]
27
+ .map((e) => e.timestamp)
28
+ .sort()[0];
29
+ return {
30
+ snapshotId: crypto.randomUUID(),
31
+ generatedAt: new Date().toISOString(),
32
+ recentDecisionRefs,
33
+ openObligations: [],
34
+ quietDebt: {
35
+ hasUnprocessedEvidence: !life.empty,
36
+ oldestUnprocessedEvidenceAt: oldest,
37
+ pendingCount,
38
+ },
39
+ fallbackRefs: [],
40
+ };
41
+ }
@@ -0,0 +1,6 @@
1
+ import type { StateDatabase } from "../db/index.js";
2
+ import type { LifeEvidenceQuery, LifeEvidenceSnapshot } from "./types.js";
3
+ export interface LoadLifeEvidenceSnapshotOptions {
4
+ runRepairGate?: boolean;
5
+ }
6
+ export declare function loadLifeEvidenceSnapshot(state: StateDatabase, workspaceRoot: string, query: LifeEvidenceQuery, options?: LoadLifeEvidenceSnapshotOptions): Promise<LifeEvidenceSnapshot>;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Bounded life evidence snapshot read model (T4.2.1).
3
+ */
4
+ import * as crypto from "node:crypto";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { and, desc, gte, inArray, lte } from "drizzle-orm";
8
+ import { lifeEvidenceIndex } from "../db/schema/life-evidence-index.js";
9
+ import { repairStateIndexes } from "../bootstrap/repair-gate.js";
10
+ function toReadModel(stored) {
11
+ return {
12
+ id: stored.id,
13
+ timestamp: stored.timestamp,
14
+ evidenceType: stored.evidenceType,
15
+ platformId: stored.platformId,
16
+ summary: stored.summary,
17
+ rawContentRef: stored.rawContentRef,
18
+ sourceRefs: stored.sourceRefs,
19
+ sensitivity: stored.sensitivity,
20
+ confidence: stored.confidence,
21
+ tags: stored.tags,
22
+ producer: stored.producer,
23
+ };
24
+ }
25
+ function buildCoverage(events) {
26
+ if (events.length === 0) {
27
+ return { coverageRatio: 1, unsupportedClaims: [], claimCoverage: [] };
28
+ }
29
+ const backed = events.filter((e) => e.sourceRefs.length > 0).length;
30
+ return {
31
+ coverageRatio: backed / events.length,
32
+ unsupportedClaims: [],
33
+ claimCoverage: events.map((e, i) => ({
34
+ claimId: `ev:${e.id}:${i}`,
35
+ backed: e.sourceRefs.length > 0,
36
+ sourceRefs: e.sourceRefs,
37
+ })),
38
+ };
39
+ }
40
+ export async function loadLifeEvidenceSnapshot(state, workspaceRoot, query, options) {
41
+ const runGate = options?.runRepairGate !== false;
42
+ if (runGate) {
43
+ const repair = await repairStateIndexes(state, { startupGate: true, workspaceRoot });
44
+ if (repair.status === "repair_required") {
45
+ const err = new Error("state_repair_required");
46
+ err.code = "repair_required";
47
+ throw err;
48
+ }
49
+ }
50
+ const now = new Date().toISOString();
51
+ const windowEnd = query.windowEnd ?? now;
52
+ const windowStart = query.windowStart ?? new Date(Date.now() - 7 * 86400000).toISOString();
53
+ const limit = query.limit ?? 50;
54
+ const conditions = [gte(lifeEvidenceIndex.timestamp, windowStart), lte(lifeEvidenceIndex.timestamp, windowEnd)];
55
+ if (query.evidenceTypes?.length) {
56
+ conditions.push(inArray(lifeEvidenceIndex.evidenceType, query.evidenceTypes));
57
+ }
58
+ const rows = await state.db
59
+ .select()
60
+ .from(lifeEvidenceIndex)
61
+ .where(and(...conditions))
62
+ .orderBy(desc(lifeEvidenceIndex.timestamp))
63
+ .limit(limit);
64
+ const platformEvents = [];
65
+ const workEvents = [];
66
+ const userInteractionEvents = [];
67
+ const quietArtifacts = [];
68
+ const evidenceRefs = [];
69
+ for (const row of rows) {
70
+ const abs = path.join(workspaceRoot, row.artifactPath.replace(/\//g, path.sep));
71
+ if (!fs.existsSync(abs)) {
72
+ continue;
73
+ }
74
+ const stored = JSON.parse(fs.readFileSync(abs, "utf-8"));
75
+ const model = toReadModel(stored);
76
+ evidenceRefs.push({
77
+ id: `ref:${model.id}`,
78
+ kind: "workspace_artifact",
79
+ uri: row.artifactPath,
80
+ });
81
+ if (model.evidenceType === "platform_browse" || model.evidenceType === "platform_interaction") {
82
+ platformEvents.push(model);
83
+ }
84
+ else if (model.evidenceType === "work_progress" || model.evidenceType === "task_discovery") {
85
+ workEvents.push(model);
86
+ }
87
+ else if (model.evidenceType === "user_interaction") {
88
+ userInteractionEvents.push(model);
89
+ }
90
+ else if (model.evidenceType === "quiet_reflection") {
91
+ quietArtifacts.push({
92
+ id: `quiet:${model.id}`,
93
+ kind: "workspace_artifact",
94
+ uri: row.artifactPath,
95
+ });
96
+ }
97
+ }
98
+ const all = [...platformEvents, ...workEvents, ...userInteractionEvents];
99
+ const coverage = buildCoverage(all);
100
+ const empty = all.length === 0;
101
+ return {
102
+ snapshotId: crypto.randomUUID(),
103
+ generatedAt: now,
104
+ windowStart,
105
+ windowEnd,
106
+ evidenceRefs,
107
+ platformEvents,
108
+ workEvents,
109
+ userInteractionEvents,
110
+ quietArtifacts,
111
+ coverage,
112
+ empty,
113
+ };
114
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Read-model snapshots aligned with state-system v5 (subset for S1/S2).
3
+ */
4
+ import type { LifeEvidenceType, SourceRef } from "../life-evidence/types.js";
5
+ export interface LifeEvidenceQuery {
6
+ windowStart?: string;
7
+ windowEnd?: string;
8
+ evidenceTypes?: LifeEvidenceType[];
9
+ limit?: number;
10
+ }
11
+ export interface SourceCoverage {
12
+ coverageRatio: number;
13
+ unsupportedClaims: string[];
14
+ claimCoverage: Array<{
15
+ claimId: string;
16
+ backed: boolean;
17
+ sourceRefs: SourceRef[];
18
+ }>;
19
+ }
20
+ export interface LifeEvidenceReadModel {
21
+ id: string;
22
+ timestamp: string;
23
+ evidenceType: LifeEvidenceType;
24
+ platformId?: string;
25
+ summary: string;
26
+ rawContentRef?: string;
27
+ sourceRefs: SourceRef[];
28
+ sensitivity: import("../life-evidence/types.js").Sensitivity;
29
+ confidence: number;
30
+ tags: string[];
31
+ producer: string;
32
+ }
33
+ export interface LifeEvidenceSnapshot {
34
+ snapshotId: string;
35
+ generatedAt: string;
36
+ windowStart: string;
37
+ windowEnd: string;
38
+ evidenceRefs: SourceRef[];
39
+ platformEvents: LifeEvidenceReadModel[];
40
+ workEvents: LifeEvidenceReadModel[];
41
+ userInteractionEvents: LifeEvidenceReadModel[];
42
+ quietArtifacts: SourceRef[];
43
+ coverage: SourceCoverage;
44
+ empty: boolean;
45
+ }
46
+ export interface ContinuitySnapshot {
47
+ snapshotId: string;
48
+ generatedAt: string;
49
+ lastHeartbeatAt?: string;
50
+ recentDecisionRefs: SourceRef[];
51
+ openObligations: SourceRef[];
52
+ quietDebt: {
53
+ hasUnprocessedEvidence: boolean;
54
+ oldestUnprocessedEvidenceAt?: string;
55
+ pendingCount: number;
56
+ };
57
+ fallbackRefs: SourceRef[];
58
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -7,6 +7,7 @@ import { PolicyRepository } from "./repositories/policy-repository.js";
7
7
  import { createDailyLogPipeline, } from "./services/daily-log-pipeline.js";
8
8
  import { createQuietInputLoader, } from "./services/quiet-input-loader.js";
9
9
  import { createPersonaCandidateLoader } from "./services/persona-candidate-loader.js";
10
+ import { encryptCredentialAtRest, isCredentialCiphertext } from "./services/credential-vault.js";
10
11
  export class DefaultStateAPI {
11
12
  database;
12
13
  read;
@@ -31,11 +32,13 @@ export class DefaultStateAPI {
31
32
  const record = await policyRepository.findByPlatformId(platformId);
32
33
  if (!record)
33
34
  return undefined;
35
+ const r = record;
34
36
  return {
35
- platformId: record.platformId ?? record.platform_id,
36
- socialDailyLimit: record.socialDailyLimit ?? record.social_daily_limit,
37
- quietEnabled: record.quietEnabled ?? Boolean(record.quiet_enabled),
38
- updatedAt: record.updatedAt ?? record.updated_at,
37
+ platformId: r.platformId ?? r.platform_id ?? platformId,
38
+ socialDailyLimit: r.socialDailyLimit ?? r.social_daily_limit ?? 0,
39
+ quietEnabled: r.quietEnabled ?? Boolean(r.quiet_enabled),
40
+ outreachDailyBudget: r.outreachDailyBudget ?? r.outreach_daily_budget ?? 2,
41
+ updatedAt: r.updatedAt ?? r.updated_at ?? "",
39
42
  };
40
43
  },
41
44
  loadPersonaCandidates: (sceneContext) => personaCandidateLoader.loadPersonaCandidates(sceneContext),
@@ -50,6 +53,7 @@ export class DefaultStateAPI {
50
53
  platformId: policy.platformId,
51
54
  socialDailyLimit: policy.socialDailyLimit,
52
55
  quietEnabled: policy.quietEnabled,
56
+ outreachDailyBudget: policy.outreachDailyBudget ?? 2,
53
57
  updatedAt: new Date().toISOString(),
54
58
  });
55
59
  },
@@ -74,8 +78,11 @@ export class DefaultStateAPI {
74
78
  },
75
79
  saveCredentialContext: async (input) => {
76
80
  const ctx = input;
81
+ const raw = ctx.encryptedValue != null ? String(ctx.encryptedValue) : "";
82
+ const encryptedValue = !raw || isCredentialCiphertext(raw) ? raw : encryptCredentialAtRest(raw);
77
83
  await credentialRepository.upsert({
78
84
  ...ctx,
85
+ encryptedValue,
79
86
  updatedAt: ctx.updatedAt ?? new Date().toISOString(),
80
87
  });
81
88
  },
@@ -0,0 +1,2 @@
1
+ import type { UserInterestSnapshot } from "./types.js";
2
+ export declare function loadUserInterestSnapshot(workspaceRoot: string): Promise<UserInterestSnapshot>;