@haaaiawd/second-nature 0.2.2 → 0.2.5

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 (53) 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 +119 -31
  6. package/runtime/connectors/base/contract.d.ts +11 -0
  7. package/runtime/connectors/base/failure-taxonomy.js +45 -26
  8. package/runtime/connectors/base/policy-bound-write-dispatch.d.ts +29 -0
  9. package/runtime/connectors/base/policy-bound-write-dispatch.js +127 -0
  10. package/runtime/connectors/services/connector-cooldown-port.d.ts +22 -0
  11. package/runtime/connectors/services/connector-cooldown-port.js +123 -0
  12. package/runtime/connectors/services/connector-executor-adapter.js +10 -4
  13. package/runtime/connectors/services/credential-route-context.d.ts +3 -2
  14. package/runtime/connectors/services/credential-route-context.js +19 -3
  15. package/runtime/core/second-nature/action/action-closure-recorder.d.ts +4 -0
  16. package/runtime/core/second-nature/action/action-closure-recorder.js +5 -0
  17. package/runtime/core/second-nature/action/action-proposal-builder.js +1 -0
  18. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +2 -0
  19. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +412 -25
  20. package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +35 -0
  21. package/runtime/core/second-nature/control-plane/real-runtime-spine.js +42 -0
  22. package/runtime/core/second-nature/guidance/impulse-context-reader.d.ts +44 -0
  23. package/runtime/core/second-nature/guidance/impulse-context-reader.js +84 -0
  24. package/runtime/core/second-nature/guidance/impulse-context-writer.d.ts +39 -0
  25. package/runtime/core/second-nature/guidance/impulse-context-writer.js +70 -0
  26. package/runtime/core/second-nature/perception/judgment-engine.d.ts +2 -0
  27. package/runtime/core/second-nature/perception/judgment-engine.js +11 -1
  28. package/runtime/core/second-nature/perception/perception-builder.d.ts +6 -2
  29. package/runtime/core/second-nature/perception/perception-builder.js +18 -7
  30. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.d.ts +43 -0
  31. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +162 -0
  32. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.d.ts +2 -2
  33. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +27 -44
  34. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +3 -0
  35. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +4 -0
  36. package/runtime/observability/living-loop-health-gate.d.ts +49 -0
  37. package/runtime/observability/living-loop-health-gate.js +141 -0
  38. package/runtime/observability/loop-status.d.ts +30 -0
  39. package/runtime/observability/loop-status.js +167 -7
  40. package/runtime/observability/services/heartbeat-digest-assembler.d.ts +21 -0
  41. package/runtime/observability/services/heartbeat-digest-assembler.js +44 -0
  42. package/runtime/shared/types/v8-contracts.d.ts +2 -2
  43. package/runtime/storage/db/index.js +60 -6
  44. package/runtime/storage/db/migrations/index.js +4 -0
  45. package/runtime/storage/db/migrations/v8-001-living-perception-loop.js +119 -119
  46. package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.d.ts +12 -0
  47. package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.js +14 -0
  48. package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.d.ts +10 -0
  49. package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.js +12 -0
  50. package/runtime/storage/db/schema/v8-entities.d.ts +874 -0
  51. package/runtime/storage/db/schema/v8-entities.js +62 -1
  52. package/runtime/storage/v8-state-stores.d.ts +41 -2
  53. package/runtime/storage/v8-state-stores.js +206 -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.5",
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.5",
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,37 @@ 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
+ hasFreshImpulseContext: realRunResult.gate.hasFreshImpulseContext,
1071
+ hasProjectionFeedback: realRunResult.gate.hasProjectionFeedback,
1072
+ missingStage: realRunResult.gate.missingStage,
1073
+ missingReason: realRunResult.gate.missingReason,
1074
+ };
1075
+ }
1076
+ else {
1077
+ digest.realRunHealth = {
1078
+ gatePassed: false,
1079
+ contractSmokeOnly: false,
1080
+ seededStateDetected: false,
1081
+ hasRealClosure: false,
1082
+ hasQuietArtifact: false,
1083
+ hasDreamArtifact: false,
1084
+ hasFreshImpulseContext: false,
1085
+ hasProjectionFeedback: false,
1086
+ missingReason: "Real-run health check degraded: " + realRunResult.degraded.reason,
1087
+ };
1088
+ }
1089
+ }
1056
1090
  const envelope = {
1057
1091
  ok: true,
1058
1092
  command: "heartbeat_digest",
@@ -1472,14 +1506,12 @@ export function createOpsRouter(deps) {
1472
1506
  return envelope;
1473
1507
  }
1474
1508
  }
1475
- // ─── T-V7C.C.4R: guidance_payload ──────────────────────────────────────
1509
+ // ─── T-V7C.C.4R + T-GVS.R.1: guidance_payload ─────────────────────────
1476
1510
  // 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.
1511
+ // When state DB is wired, reads persisted artifact first; falls back to
1512
+ // real-time assembly and persists for subsequent reads.
1479
1513
  if (command === "guidance_payload") {
1480
1514
  const generatedAt = new Date().toISOString();
1481
- const { assembleImpulseSync } = await import("../../guidance/impulse-assembler.js");
1482
- const { getBaselineAtmosphereTemplate } = await import("../../guidance/template-registry.js");
1483
1515
  const sceneType = input?.sceneType ?? "social";
1484
1516
  const capabilityIntent = typeof input?.capabilityIntent === "string"
1485
1517
  ? input.capabilityIntent
@@ -1505,22 +1537,53 @@ export function createOpsRouter(deps) {
1505
1537
  };
1506
1538
  return envelope;
1507
1539
  }
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: {
1540
+ // T-GVS.R.1: Try reading persisted artifact first
1541
+ let artifactData;
1542
+ let warnings = [];
1543
+ let sourceRefs = [
1544
+ "guidance/capability-class.ts",
1545
+ "guidance/impulse-assembler.ts",
1546
+ "guidance/template-registry.ts",
1547
+ "guidance/output-guard.ts",
1548
+ ];
1549
+ if (deps.state) {
1550
+ try {
1551
+ const { readImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-reader.js");
1552
+ const existing = await readImpulseContext(deps.state, sceneType, capabilityIntent, platformId);
1553
+ if (existing.available) {
1554
+ artifactData = {
1555
+ sceneType: existing.artifact.sceneType,
1556
+ capabilityIntent: existing.artifact.capabilityIntent,
1557
+ platformId: existing.artifact.platformId,
1558
+ capabilityClass: existing.artifact.capabilityClass,
1559
+ impulseSource: existing.artifact.impulseSource,
1560
+ impulseText: existing.artifact.impulseText,
1561
+ atmosphereText: existing.artifact.atmosphereText,
1562
+ expressionBoundaryConstraints: existing.artifact.expressionBoundaryConstraints,
1563
+ expressionBoundaryStyle: existing.artifact.expressionBoundaryStyle,
1564
+ freshnessMs: existing.freshnessMs,
1565
+ persisted: true,
1566
+ };
1567
+ sourceRefs.push("core/second-nature/guidance/impulse-context-reader.ts");
1568
+ }
1569
+ }
1570
+ catch {
1571
+ // Reader failure → fall through to assembly
1572
+ }
1573
+ }
1574
+ // Real-time assembly if no persisted artifact
1575
+ if (!artifactData) {
1576
+ const { assembleImpulseSync } = await import("../../guidance/impulse-assembler.js");
1577
+ const { buildExpressionBoundary } = await import("../../guidance/output-guard.js");
1578
+ const { getShortAtmosphereTemplate } = await import("../../guidance/template-registry.js");
1579
+ const impulseResult = assembleImpulseSync({
1580
+ sceneType: sceneType,
1581
+ capabilityIntent,
1582
+ platformId,
1583
+ });
1584
+ const atmosphere = getShortAtmosphereTemplate("active", "low");
1585
+ const expressionBoundary = buildExpressionBoundary(sceneType);
1586
+ artifactData = {
1524
1587
  sceneType,
1525
1588
  capabilityIntent: capabilityIntent ?? null,
1526
1589
  platformId: platformId ?? null,
@@ -1532,16 +1595,41 @@ export function createOpsRouter(deps) {
1532
1595
  atmosphereReviewStatus: atmosphere.reviewStatus,
1533
1596
  expressionBoundaryConstraints: expressionBoundary.constraints,
1534
1597
  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
- ],
1598
+ persisted: false,
1599
+ };
1600
+ if (impulseResult.source === "none") {
1601
+ warnings.push("no_impulse_available_for_this_scene_and_capability");
1602
+ }
1603
+ // T-GVS.R.1: Persist assembled artifact for future reads
1604
+ if (deps.state) {
1605
+ try {
1606
+ const { writeImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-writer.js");
1607
+ await writeImpulseContext(deps.state, {
1608
+ sceneType,
1609
+ capabilityIntent,
1610
+ platformId,
1611
+ impulseResult,
1612
+ atmosphereText: atmosphere.text,
1613
+ expressionBoundaryConstraints: expressionBoundary.constraints,
1614
+ expressionBoundaryStyle: expressionBoundary.style,
1615
+ }, { now: generatedAt });
1616
+ sourceRefs.push("core/second-nature/guidance/impulse-context-writer.ts");
1617
+ }
1618
+ catch {
1619
+ // Persistence failure is non-fatal; surface still returns assembled payload
1620
+ warnings.push("impulse_context_persistence_failed");
1621
+ }
1622
+ }
1623
+ }
1624
+ const envelope = {
1625
+ ok: true,
1626
+ command: "guidance_payload",
1627
+ runtimeMode: deps.runtimeAvailable ? "workspace_full_runtime" : "host_safe_carrier",
1628
+ surfaceMode: "cli",
1629
+ generatedAt,
1630
+ data: artifactData,
1631
+ warnings,
1632
+ sourceRefs,
1545
1633
  };
1546
1634
  return envelope;
1547
1635
  }
@@ -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;
@@ -63,6 +73,7 @@ export interface CooldownLedgerPort {
63
73
  loadCooldownState(platformId: string, intent: CapabilityIntent): Promise<{
64
74
  blocked: boolean;
65
75
  retryAfterMs?: number;
76
+ reason?: string;
66
77
  }>;
67
78
  }
68
79
  export interface RouteContextPort extends CredentialContextPort, CooldownLedgerPort {
@@ -61,6 +61,23 @@ function readRetryAfterMs(input) {
61
61
  }
62
62
  return undefined;
63
63
  }
64
+ function readStatusCode(record) {
65
+ if (typeof record.status === "number")
66
+ return record.status;
67
+ if (typeof record.statusCode === "number")
68
+ return record.statusCode;
69
+ if (typeof record.status === "string") {
70
+ const parsed = Number.parseInt(record.status, 10);
71
+ if (Number.isFinite(parsed))
72
+ return parsed;
73
+ }
74
+ if (typeof record.statusCode === "string") {
75
+ const parsed = Number.parseInt(record.statusCode, 10);
76
+ if (Number.isFinite(parsed))
77
+ return parsed;
78
+ }
79
+ return undefined;
80
+ }
64
81
  export function classifyFailure(error) {
65
82
  if (error instanceof ConnectorPolicyError) {
66
83
  return {
@@ -74,6 +91,34 @@ export function classifyFailure(error) {
74
91
  }
75
92
  if (error && typeof error === "object") {
76
93
  const record = error;
94
+ const status = readStatusCode(record);
95
+ if (status !== undefined) {
96
+ if (status === 429) {
97
+ return {
98
+ class: "rate_limited",
99
+ retryable: RETRYABLE_BY_CLASS.rate_limited,
100
+ retryAfterMs: readRetryAfterMs(record),
101
+ };
102
+ }
103
+ if (status === 401 || status === 403) {
104
+ return {
105
+ class: "auth_failure",
106
+ retryable: RETRYABLE_BY_CLASS.auth_failure,
107
+ };
108
+ }
109
+ if (status === 400 || status === 404 || status === 422) {
110
+ return {
111
+ class: "permanent_input_error",
112
+ retryable: RETRYABLE_BY_CLASS.permanent_input_error,
113
+ };
114
+ }
115
+ if (status >= 500 && status <= 599) {
116
+ return {
117
+ class: "transport_failure",
118
+ retryable: RETRYABLE_BY_CLASS.transport_failure,
119
+ };
120
+ }
121
+ }
77
122
  const code = record.code;
78
123
  if (typeof code === "string") {
79
124
  if (code === "auth_failure")
@@ -152,32 +197,6 @@ export function classifyFailure(error) {
152
197
  retryable: RETRYABLE_BY_CLASS.unknown_platform_change,
153
198
  };
154
199
  }
155
- const status = record.status;
156
- if (status === 429) {
157
- return {
158
- class: "rate_limited",
159
- retryable: RETRYABLE_BY_CLASS.rate_limited,
160
- retryAfterMs: readRetryAfterMs(record),
161
- };
162
- }
163
- if (status === 401 || status === 403) {
164
- return {
165
- class: "auth_failure",
166
- retryable: RETRYABLE_BY_CLASS.auth_failure,
167
- };
168
- }
169
- if (status === 400 || status === 404 || status === 422) {
170
- return {
171
- class: "permanent_input_error",
172
- retryable: RETRYABLE_BY_CLASS.permanent_input_error,
173
- };
174
- }
175
- if (status === 500 || status === 502 || status === 503 || status === 504) {
176
- return {
177
- class: "transport_failure",
178
- retryable: RETRYABLE_BY_CLASS.transport_failure,
179
- };
180
- }
181
200
  }
182
201
  return {
183
202
  class: "unknown_platform_change",
@@ -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
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * ConnectorCooldownPort — Durable cooldown ledger for repeated terminal failures.
3
+ *
4
+ * Core logic: Track terminal failures per platform/capability and block replay
5
+ * for a bounded window after repeated failures. Successful recovery is allowed
6
+ * to bypass stale cooldown.
7
+ *
8
+ * Design authority:
9
+ * - `.anws/v8/04_SYSTEM_DESIGN/connector-system.md §6`
10
+ * - `.anws/v8/04_SYSTEM_DESIGN/body-tool-system.md §4`
11
+ *
12
+ * Dependencies:
13
+ * - `src/storage/v8-state-stores.js` (read/write connector cooldown state)
14
+ * - `src/connectors/base/failure-taxonomy.js` (FailureClass, retryable lookup)
15
+ *
16
+ * Boundary:
17
+ * - Does not execute connectors; only records/read cooldown state.
18
+ * - Does not permanently blacklist platforms; cooldown expires.
19
+ */
20
+ import type { StateDatabase } from "../../storage/db/index.js";
21
+ import type { CooldownPort } from "../base/policy-layer.js";
22
+ export declare function createConnectorCooldownPort(db: StateDatabase): CooldownPort;