@haaaiawd/second-nature 0.2.2 → 0.2.4

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 (42) hide show
  1. package/openclaw.plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/runtime/cli/ops/heartbeat-surface.d.ts +20 -0
  4. package/runtime/cli/ops/heartbeat-surface.js +72 -1
  5. package/runtime/cli/ops/ops-router.js +115 -31
  6. package/runtime/connectors/base/contract.d.ts +10 -0
  7. package/runtime/connectors/base/policy-bound-write-dispatch.d.ts +29 -0
  8. package/runtime/connectors/base/policy-bound-write-dispatch.js +127 -0
  9. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +336 -25
  10. package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +33 -0
  11. package/runtime/core/second-nature/control-plane/real-runtime-spine.js +41 -0
  12. package/runtime/core/second-nature/guidance/impulse-context-reader.d.ts +44 -0
  13. package/runtime/core/second-nature/guidance/impulse-context-reader.js +84 -0
  14. package/runtime/core/second-nature/guidance/impulse-context-writer.d.ts +39 -0
  15. package/runtime/core/second-nature/guidance/impulse-context-writer.js +70 -0
  16. package/runtime/core/second-nature/perception/judgment-engine.d.ts +2 -0
  17. package/runtime/core/second-nature/perception/judgment-engine.js +11 -1
  18. package/runtime/core/second-nature/perception/perception-builder.d.ts +6 -2
  19. package/runtime/core/second-nature/perception/perception-builder.js +18 -7
  20. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.d.ts +43 -0
  21. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +157 -0
  22. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +17 -16
  23. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +3 -0
  24. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +4 -0
  25. package/runtime/observability/living-loop-health-gate.d.ts +45 -0
  26. package/runtime/observability/living-loop-health-gate.js +94 -0
  27. package/runtime/observability/loop-status.d.ts +11 -0
  28. package/runtime/observability/loop-status.js +49 -3
  29. package/runtime/observability/services/heartbeat-digest-assembler.d.ts +12 -0
  30. package/runtime/observability/services/heartbeat-digest-assembler.js +9 -0
  31. package/runtime/shared/types/v8-contracts.d.ts +2 -2
  32. package/runtime/storage/db/index.js +34 -0
  33. package/runtime/storage/db/migrations/index.js +4 -0
  34. package/runtime/storage/db/migrations/v8-001-living-perception-loop.js +119 -119
  35. package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.d.ts +12 -0
  36. package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.js +14 -0
  37. package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.d.ts +10 -0
  38. package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.js +12 -0
  39. package/runtime/storage/db/schema/v8-entities.d.ts +586 -0
  40. package/runtime/storage/db/schema/v8-entities.js +39 -0
  41. package/runtime/storage/v8-state-stores.d.ts +32 -2
  42. package/runtime/storage/v8-state-stores.js +121 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.2.2",
4
+ "version": "0.2.4",
5
5
  "description": "OpenClaw native plugin with synchronous surface registration and bundled runtime spine. Set SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot to the same path as the agent workspace. Agent inner guide is packaged as agent-inner-guide.md. v7 ops surface: self_health, tool_affordance, heartbeat_digest, snapshot:capture, narrative:diff, timeline, restore, runtime_secret_bootstrap, connector:run, guidance_payload.",
6
6
  "activation": {
7
7
  "onStartup": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/second-nature",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "OpenClaw native plugin with synchronous registration, a packaged runtime artifact, and operator-facing status/explain flows.",
5
5
  "keywords": [
6
6
  "openclaw",
@@ -19,6 +19,7 @@ import type { GoalLifecyclePolicy } from "../../core/second-nature/heartbeat/goa
19
19
  import type { IdleCuriosityPolicy } from "../../core/second-nature/heartbeat/idle-curiosity-policy.js";
20
20
  import type { CircuitBreakerManager } from "../../core/second-nature/body/circuit-breaker/circuit-breaker-manager.js";
21
21
  import type { AppendOnlyAuditStore } from "../../observability/audit/append-only-audit-store.js";
22
+ import { type RealRuntimeSpineResult } from "../../core/second-nature/control-plane/real-runtime-spine.js";
22
23
  export type HeartbeatSurfaceStatus = "heartbeat_ok" | "intent_selected" | "denied" | "deferred" | "runtime_carrier_only" | "delivery_unavailable";
23
24
  export interface HeartbeatSurfaceResult {
24
25
  ok: boolean;
@@ -33,6 +34,20 @@ export interface HeartbeatSurfaceResult {
33
34
  livedExperienceLoopClaimed: boolean;
34
35
  /** True when structured fields mirror a fake adapter for schema parity only */
35
36
  schemaParityOnly?: boolean;
37
+ /** T-CP.R.2: v8 real runtime spine result when state-backed action-closure spine ran */
38
+ v8Spine?: RealRuntimeSpineResult & {
39
+ degradedReason?: string;
40
+ };
41
+ /** T-GVS.R.1: agent-facing impulse context artifact read pointer */
42
+ impulseContext?: {
43
+ available: boolean;
44
+ sceneType?: string;
45
+ capabilityClass?: string | null;
46
+ impulseText?: string | null;
47
+ atmosphereText?: string | null;
48
+ freshnessMs?: number;
49
+ missingReason?: string;
50
+ };
36
51
  }
37
52
  export interface HeartbeatCheckInput {
38
53
  probeOnly?: boolean;
@@ -83,5 +98,10 @@ export interface HeartbeatCheckInput {
83
98
  circuitBreakerManager?: CircuitBreakerManager;
84
99
  /** T-OBS.R.1: shared audit sink for connector/Quiet events consumed by heartbeat_digest. */
85
100
  auditStore?: AppendOnlyAuditStore;
101
+ /**
102
+ * T-CP.R.2: when true and state DB is wired, runs the v8 real runtime action-closure spine
103
+ * in addition to the v7 heartbeat loop. Produces state-backed closure/no-action records.
104
+ */
105
+ v8SpineEnabled?: boolean;
86
106
  }
87
107
  export declare function heartbeatCheck(input: HeartbeatCheckInput): Promise<HeartbeatSurfaceResult>;
@@ -1,4 +1,6 @@
1
1
  import { createWorkspaceHeartbeatRunner } from "./workspace-heartbeat-runner.js";
2
+ // T-CP.R.2: v8 real runtime spine bridge
3
+ import { runRealRuntimeHeartbeatCycle, } from "../../core/second-nature/control-plane/real-runtime-spine.js";
2
4
  function mapCycleToSurface(cycle, surfaceMode) {
3
5
  const status = cycle.status === "runtime_carrier_only"
4
6
  ? "runtime_carrier_only"
@@ -86,7 +88,76 @@ export async function heartbeatCheck(input) {
86
88
  });
87
89
  try {
88
90
  const cycle = await run(signal);
89
- return mapCycleToSurface(cycle, "workspace_full_runtime");
91
+ const surfaceResult = mapCycleToSurface(cycle, "workspace_full_runtime");
92
+ // T-CP.R.2: run v8 real runtime spine when enabled and state is available
93
+ if (input.v8SpineEnabled && input.state && input.workspaceRoot) {
94
+ try {
95
+ const v8Result = await runRealRuntimeHeartbeatCycle({
96
+ workspaceRoot: input.workspaceRoot,
97
+ state: input.state,
98
+ requestedAt: timestamp,
99
+ trigger: "host",
100
+ });
101
+ if ("status" in v8Result && v8Result.status === "degraded") {
102
+ surfaceResult.v8Spine = {
103
+ cycleId: "",
104
+ cycleSequence: 0,
105
+ degradedReason: v8Result.reason,
106
+ };
107
+ surfaceResult.reasons = [
108
+ ...surfaceResult.reasons,
109
+ `v8_spine_degraded:${v8Result.reason}`,
110
+ ];
111
+ }
112
+ else {
113
+ const spine = v8Result;
114
+ surfaceResult.v8Spine = spine;
115
+ surfaceResult.reasons = [
116
+ ...surfaceResult.reasons,
117
+ `v8_spine_cycle:${spine.cycleId}`,
118
+ spine.closureRef
119
+ ? "v8_closure_recorded"
120
+ : `v8_no_action:${spine.noActionReason ?? "unknown"}`,
121
+ ];
122
+ }
123
+ }
124
+ catch (v8Err) {
125
+ const v8Msg = v8Err instanceof Error ? v8Err.message : String(v8Err);
126
+ surfaceResult.reasons = [
127
+ ...surfaceResult.reasons,
128
+ `v8_spine_exception:${v8Msg.slice(0, 120)}`,
129
+ ];
130
+ }
131
+ }
132
+ // T-GVS.R.1: expose impulse context artifact when state is available
133
+ if (input.state) {
134
+ try {
135
+ const { readImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-reader.js");
136
+ const ctx = await readImpulseContext(input.state, "social");
137
+ if (ctx.available) {
138
+ surfaceResult.impulseContext = {
139
+ available: true,
140
+ sceneType: ctx.artifact.sceneType,
141
+ capabilityClass: ctx.artifact.capabilityClass,
142
+ impulseText: ctx.artifact.impulseText,
143
+ atmosphereText: ctx.artifact.atmosphereText,
144
+ freshnessMs: ctx.freshnessMs,
145
+ };
146
+ surfaceResult.reasons.push(`impulse_context:${ctx.artifact.id}`);
147
+ }
148
+ else {
149
+ surfaceResult.impulseContext = {
150
+ available: false,
151
+ missingReason: ctx.reason,
152
+ };
153
+ surfaceResult.reasons.push(`impulse_context_missing:${ctx.reason}`);
154
+ }
155
+ }
156
+ catch {
157
+ // Non-fatal: impulse context is advisory
158
+ }
159
+ }
160
+ return surfaceResult;
90
161
  }
91
162
  catch (err) {
92
163
  const msg = err instanceof Error ? err.message : String(err);
@@ -28,6 +28,7 @@ import { writeRestoreAudit, } from "../../observability/services/restore-audit-s
28
28
  import { createHistoryDigestStore } from "../../storage/services/history-digest-store.js";
29
29
  // v8 T-ROS.C.1: loop_status read model
30
30
  import { readLoopStatus } from "../../observability/loop-status.js";
31
+ import { checkRealRunHealth } from "../../observability/living-loop-health-gate.js";
31
32
  // T-ROS.C.3: ManualRunDispatcher and its deps
32
33
  import { createManualRunDispatcher, } from "./manual-run-dispatcher.js";
33
34
  import { createExperienceWriter } from "../../core/second-nature/body/tool-experience/experience-writer.js";
@@ -445,6 +446,8 @@ export function createOpsRouter(deps) {
445
446
  goalLifecyclePolicy,
446
447
  idleCuriosityPolicy,
447
448
  circuitBreakerManager,
449
+ v8SpineEnabled: input
450
+ ?.v8SpineEnabled ?? (deps.state !== undefined),
448
451
  });
449
452
  if (result.ok &&
450
453
  result.surfaceMode === "workspace_full_runtime" &&
@@ -1053,6 +1056,33 @@ export function createOpsRouter(deps) {
1053
1056
  ...deps.heartbeatDigestDeps,
1054
1057
  };
1055
1058
  const digest = await generateHeartbeatDigest(date, digestDeps);
1059
+ // T-OBS.R.3: Embed real-run health into digest when state DB is available
1060
+ if (deps.state) {
1061
+ const realRunResult = await checkRealRunHealth(deps.state, date);
1062
+ if (realRunResult.ok) {
1063
+ digest.realRunHealth = {
1064
+ gatePassed: realRunResult.gate.gatePassed,
1065
+ contractSmokeOnly: realRunResult.gate.contractSmokeOnly,
1066
+ seededStateDetected: realRunResult.gate.seededStateDetected,
1067
+ hasRealClosure: realRunResult.gate.hasRealClosure,
1068
+ hasQuietArtifact: realRunResult.gate.hasQuietArtifact,
1069
+ hasDreamArtifact: realRunResult.gate.hasDreamArtifact,
1070
+ missingStage: realRunResult.gate.missingStage,
1071
+ missingReason: realRunResult.gate.missingReason,
1072
+ };
1073
+ }
1074
+ else {
1075
+ digest.realRunHealth = {
1076
+ gatePassed: false,
1077
+ contractSmokeOnly: false,
1078
+ seededStateDetected: false,
1079
+ hasRealClosure: false,
1080
+ hasQuietArtifact: false,
1081
+ hasDreamArtifact: false,
1082
+ missingReason: "Real-run health check degraded: " + realRunResult.degraded.reason,
1083
+ };
1084
+ }
1085
+ }
1056
1086
  const envelope = {
1057
1087
  ok: true,
1058
1088
  command: "heartbeat_digest",
@@ -1472,14 +1502,12 @@ export function createOpsRouter(deps) {
1472
1502
  return envelope;
1473
1503
  }
1474
1504
  }
1475
- // ─── T-V7C.C.4R: guidance_payload ──────────────────────────────────────
1505
+ // ─── T-V7C.C.4R + T-GVS.R.1: guidance_payload ─────────────────────────
1476
1506
  // Returns the assembled impulse + atmosphere for a given scene context.
1477
- // Useful for Claw to inspect what guidance content would be injected before
1478
- // a real heartbeat cycle, and to verify platform-specific impulse overrides.
1507
+ // When state DB is wired, reads persisted artifact first; falls back to
1508
+ // real-time assembly and persists for subsequent reads.
1479
1509
  if (command === "guidance_payload") {
1480
1510
  const generatedAt = new Date().toISOString();
1481
- const { assembleImpulseSync } = await import("../../guidance/impulse-assembler.js");
1482
- const { getBaselineAtmosphereTemplate } = await import("../../guidance/template-registry.js");
1483
1511
  const sceneType = input?.sceneType ?? "social";
1484
1512
  const capabilityIntent = typeof input?.capabilityIntent === "string"
1485
1513
  ? input.capabilityIntent
@@ -1505,22 +1533,53 @@ export function createOpsRouter(deps) {
1505
1533
  };
1506
1534
  return envelope;
1507
1535
  }
1508
- const impulseResult = assembleImpulseSync({
1509
- sceneType: sceneType,
1510
- capabilityIntent,
1511
- platformId,
1512
- });
1513
- const { buildExpressionBoundary } = await import("../../guidance/output-guard.js");
1514
- const { getShortAtmosphereTemplate } = await import("../../guidance/template-registry.js");
1515
- const atmosphere = getShortAtmosphereTemplate("active", "low");
1516
- const expressionBoundary = buildExpressionBoundary(sceneType);
1517
- const envelope = {
1518
- ok: true,
1519
- command: "guidance_payload",
1520
- runtimeMode: deps.runtimeAvailable ? "workspace_full_runtime" : "host_safe_carrier",
1521
- surfaceMode: "cli",
1522
- generatedAt,
1523
- data: {
1536
+ // T-GVS.R.1: Try reading persisted artifact first
1537
+ let artifactData;
1538
+ let warnings = [];
1539
+ let sourceRefs = [
1540
+ "guidance/capability-class.ts",
1541
+ "guidance/impulse-assembler.ts",
1542
+ "guidance/template-registry.ts",
1543
+ "guidance/output-guard.ts",
1544
+ ];
1545
+ if (deps.state) {
1546
+ try {
1547
+ const { readImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-reader.js");
1548
+ const existing = await readImpulseContext(deps.state, sceneType, capabilityIntent, platformId);
1549
+ if (existing.available) {
1550
+ artifactData = {
1551
+ sceneType: existing.artifact.sceneType,
1552
+ capabilityIntent: existing.artifact.capabilityIntent,
1553
+ platformId: existing.artifact.platformId,
1554
+ capabilityClass: existing.artifact.capabilityClass,
1555
+ impulseSource: existing.artifact.impulseSource,
1556
+ impulseText: existing.artifact.impulseText,
1557
+ atmosphereText: existing.artifact.atmosphereText,
1558
+ expressionBoundaryConstraints: existing.artifact.expressionBoundaryConstraints,
1559
+ expressionBoundaryStyle: existing.artifact.expressionBoundaryStyle,
1560
+ freshnessMs: existing.freshnessMs,
1561
+ persisted: true,
1562
+ };
1563
+ sourceRefs.push("core/second-nature/guidance/impulse-context-reader.ts");
1564
+ }
1565
+ }
1566
+ catch {
1567
+ // Reader failure → fall through to assembly
1568
+ }
1569
+ }
1570
+ // Real-time assembly if no persisted artifact
1571
+ if (!artifactData) {
1572
+ const { assembleImpulseSync } = await import("../../guidance/impulse-assembler.js");
1573
+ const { buildExpressionBoundary } = await import("../../guidance/output-guard.js");
1574
+ const { getShortAtmosphereTemplate } = await import("../../guidance/template-registry.js");
1575
+ const impulseResult = assembleImpulseSync({
1576
+ sceneType: sceneType,
1577
+ capabilityIntent,
1578
+ platformId,
1579
+ });
1580
+ const atmosphere = getShortAtmosphereTemplate("active", "low");
1581
+ const expressionBoundary = buildExpressionBoundary(sceneType);
1582
+ artifactData = {
1524
1583
  sceneType,
1525
1584
  capabilityIntent: capabilityIntent ?? null,
1526
1585
  platformId: platformId ?? null,
@@ -1532,16 +1591,41 @@ export function createOpsRouter(deps) {
1532
1591
  atmosphereReviewStatus: atmosphere.reviewStatus,
1533
1592
  expressionBoundaryConstraints: expressionBoundary.constraints,
1534
1593
  expressionBoundaryStyle: expressionBoundary.style,
1535
- },
1536
- warnings: impulseResult.source === "none"
1537
- ? ["no_impulse_available_for_this_scene_and_capability"]
1538
- : [],
1539
- sourceRefs: [
1540
- "guidance/capability-class.ts",
1541
- "guidance/impulse-assembler.ts",
1542
- "guidance/template-registry.ts",
1543
- "guidance/output-guard.ts",
1544
- ],
1594
+ persisted: false,
1595
+ };
1596
+ if (impulseResult.source === "none") {
1597
+ warnings.push("no_impulse_available_for_this_scene_and_capability");
1598
+ }
1599
+ // T-GVS.R.1: Persist assembled artifact for future reads
1600
+ if (deps.state) {
1601
+ try {
1602
+ const { writeImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-writer.js");
1603
+ await writeImpulseContext(deps.state, {
1604
+ sceneType,
1605
+ capabilityIntent,
1606
+ platformId,
1607
+ impulseResult,
1608
+ atmosphereText: atmosphere.text,
1609
+ expressionBoundaryConstraints: expressionBoundary.constraints,
1610
+ expressionBoundaryStyle: expressionBoundary.style,
1611
+ }, { now: generatedAt });
1612
+ sourceRefs.push("core/second-nature/guidance/impulse-context-writer.ts");
1613
+ }
1614
+ catch {
1615
+ // Persistence failure is non-fatal; surface still returns assembled payload
1616
+ warnings.push("impulse_context_persistence_failed");
1617
+ }
1618
+ }
1619
+ }
1620
+ const envelope = {
1621
+ ok: true,
1622
+ command: "guidance_payload",
1623
+ runtimeMode: deps.runtimeAvailable ? "workspace_full_runtime" : "host_safe_carrier",
1624
+ surfaceMode: "cli",
1625
+ generatedAt,
1626
+ data: artifactData,
1627
+ warnings,
1628
+ sourceRefs,
1545
1629
  };
1546
1630
  return envelope;
1547
1631
  }
@@ -12,6 +12,14 @@ export interface ConnectorRequestIdentity {
12
12
  /** Canonical name across all platforms. */
13
13
  canonicalName?: string;
14
14
  }
15
+ export interface PolicyProof {
16
+ decisionId: string;
17
+ decision: "allow" | "defer" | "downgrade" | "deny";
18
+ ownerConfirmMode?: boolean;
19
+ ownerConfirmed?: boolean;
20
+ dryRun?: boolean;
21
+ reason?: string;
22
+ }
15
23
  export interface ConnectorRequest {
16
24
  platformId: string;
17
25
  intent: CapabilityIntent;
@@ -23,6 +31,8 @@ export interface ConnectorRequest {
23
31
  intentId?: string;
24
32
  /** T-V7C.C.4: identity for connector request (readable, no credential). */
25
33
  identity?: ConnectorRequestIdentity;
34
+ /** T-CS.R.1: policy proof for write-side actions */
35
+ policyProof?: PolicyProof;
26
36
  }
27
37
  export interface ExecutionPlan {
28
38
  platformId: string;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * PolicyBoundWriteDispatch — Write-side connector dispatch with policy proof gate.
3
+ *
4
+ * Core logic: Verify policy proof before external write; reject without platform
5
+ * call when proof is missing, deny, or lacks owner confirmation for high-risk
6
+ * actions. Support dry-run mode for safe testing.
7
+ *
8
+ * Design authority:
9
+ * - `.anws/v8/04_SYSTEM_DESIGN/connector-system.md §2`
10
+ * - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.detail.md §3.3`
11
+ * - `docs/validation/openclaw-plugin-classification.md §5`
12
+ *
13
+ * Dependencies:
14
+ * - `src/connectors/base/contract.js` (ConnectorRequest, ConnectorResult, PolicyProof)
15
+ * - `src/connectors/base/policy-layer.js` (createConnectorPolicyLayer)
16
+ *
17
+ * Boundary:
18
+ * - Does NOT bypass ActionPolicyDecision.
19
+ * - Does NOT execute write without valid policy proof.
20
+ * - Does NOT leak credentials in returned results.
21
+ */
22
+ import type { ConnectorRequest, ConnectorResult } from "./contract.js";
23
+ export interface WriteDispatchResult {
24
+ status: "allowed" | "denied" | "deferred" | "dry_run" | "downgraded";
25
+ reason: string;
26
+ connectorResult?: ConnectorResult<unknown>;
27
+ simulatedPayload?: Record<string, unknown>;
28
+ }
29
+ export declare function dispatchPolicyBoundWrite(request: ConnectorRequest, executeConnector: (req: ConnectorRequest) => Promise<ConnectorResult<unknown>>): Promise<WriteDispatchResult>;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * PolicyBoundWriteDispatch — Write-side connector dispatch with policy proof gate.
3
+ *
4
+ * Core logic: Verify policy proof before external write; reject without platform
5
+ * call when proof is missing, deny, or lacks owner confirmation for high-risk
6
+ * actions. Support dry-run mode for safe testing.
7
+ *
8
+ * Design authority:
9
+ * - `.anws/v8/04_SYSTEM_DESIGN/connector-system.md §2`
10
+ * - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.detail.md §3.3`
11
+ * - `docs/validation/openclaw-plugin-classification.md §5`
12
+ *
13
+ * Dependencies:
14
+ * - `src/connectors/base/contract.js` (ConnectorRequest, ConnectorResult, PolicyProof)
15
+ * - `src/connectors/base/policy-layer.js` (createConnectorPolicyLayer)
16
+ *
17
+ * Boundary:
18
+ * - Does NOT bypass ActionPolicyDecision.
19
+ * - Does NOT execute write without valid policy proof.
20
+ * - Does NOT leak credentials in returned results.
21
+ */
22
+ // ───────────────────────────────────────────────────────────────
23
+ // Helpers
24
+ // ───────────────────────────────────────────────────────────────
25
+ function isWriteCapability(intent) {
26
+ const writeIntents = ["post.publish", "comment.reply", "message.send"];
27
+ return writeIntents.includes(intent);
28
+ }
29
+ function validatePolicyProof(proof, intent) {
30
+ if (!proof) {
31
+ return {
32
+ valid: false,
33
+ reason: "policy_denied_missing_permission",
34
+ };
35
+ }
36
+ if (proof.decision === "deny") {
37
+ return {
38
+ valid: false,
39
+ reason: proof.reason || "policy_denied_high_risk",
40
+ };
41
+ }
42
+ if (proof.decision === "defer") {
43
+ return {
44
+ valid: false,
45
+ reason: "policy_deferred_owner_confirmation",
46
+ };
47
+ }
48
+ if (proof.decision === "downgrade") {
49
+ return {
50
+ valid: false,
51
+ reason: proof.reason || "policy_downgraded_to_draft",
52
+ };
53
+ }
54
+ // Allow decision
55
+ if (proof.decision !== "allow") {
56
+ return {
57
+ valid: false,
58
+ reason: "policy_denied_missing_permission",
59
+ };
60
+ }
61
+ // For write capabilities, require owner-confirm, dry-run, or explicit owner-confirmed flag
62
+ if (isWriteCapability(intent) &&
63
+ !proof.ownerConfirmMode &&
64
+ !proof.dryRun &&
65
+ !proof.ownerConfirmed) {
66
+ return {
67
+ valid: false,
68
+ reason: "policy_denied_owner_confirm_required",
69
+ };
70
+ }
71
+ return { valid: true, reason: "policy_allowed" };
72
+ }
73
+ // ───────────────────────────────────────────────────────────────
74
+ // Public API
75
+ // ───────────────────────────────────────────────────────────────
76
+ export async function dispatchPolicyBoundWrite(request, executeConnector) {
77
+ const { intent, policyProof, payload } = request;
78
+ // Only gate write capabilities
79
+ if (!isWriteCapability(intent)) {
80
+ // Read capabilities pass through to normal execution
81
+ const result = await executeConnector(request);
82
+ return {
83
+ status: "allowed",
84
+ reason: "read_capability_no_policy_gate",
85
+ connectorResult: result,
86
+ };
87
+ }
88
+ // Validate policy proof
89
+ const validation = validatePolicyProof(policyProof, intent);
90
+ if (!validation.valid) {
91
+ return {
92
+ status: validation.reason === "policy_deferred_owner_confirmation"
93
+ ? "deferred"
94
+ : "denied",
95
+ reason: validation.reason,
96
+ };
97
+ }
98
+ // Dry-run mode: simulate execution without platform call
99
+ if (policyProof?.dryRun) {
100
+ return {
101
+ status: "dry_run",
102
+ reason: "dry_run_simulated_success",
103
+ simulatedPayload: {
104
+ ...payload,
105
+ _simulated: true,
106
+ _idempotencyKey: request.idempotencyKey,
107
+ _decisionId: policyProof.decisionId,
108
+ },
109
+ };
110
+ }
111
+ // Owner-confirm mode without explicit approval: defer to owner approval
112
+ if (policyProof?.ownerConfirmMode && !policyProof?.ownerConfirmed) {
113
+ return {
114
+ status: "deferred",
115
+ reason: "owner_confirm_pending",
116
+ };
117
+ }
118
+ // Full allow → execute connector
119
+ const result = await executeConnector(request);
120
+ return {
121
+ status: result.status === "success" ? "allowed" : "denied",
122
+ reason: result.status === "success"
123
+ ? "execution_completed"
124
+ : (result.failureClass || "execution_failed"),
125
+ connectorResult: result,
126
+ };
127
+ }