@haaaiawd/second-nature 0.1.19 → 0.1.21

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 (29) hide show
  1. package/index.js +86 -30
  2. package/openclaw.plugin.json +1 -1
  3. package/package.json +1 -1
  4. package/runtime/cli/commands/index.js +37 -6
  5. package/runtime/cli/index.js +8 -0
  6. package/runtime/cli/ops/heartbeat-surface.d.ts +6 -0
  7. package/runtime/cli/ops/heartbeat-surface.js +1 -0
  8. package/runtime/cli/ops/ops-router.d.ts +12 -0
  9. package/runtime/cli/ops/ops-router.js +89 -0
  10. package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +6 -0
  11. package/runtime/cli/ops/workspace-heartbeat-runner.js +1 -0
  12. package/runtime/cli/read-models/index.d.ts +12 -1
  13. package/runtime/cli/read-models/index.js +26 -0
  14. package/runtime/cli/read-models/types.d.ts +18 -1
  15. package/runtime/connectors/base/contract.d.ts +10 -0
  16. package/runtime/connectors/base/contract.js +10 -2
  17. package/runtime/connectors/base/failure-taxonomy.js +63 -15
  18. package/runtime/connectors/services/connector-executor-adapter.d.ts +16 -0
  19. package/runtime/connectors/services/connector-executor-adapter.js +118 -0
  20. package/runtime/connectors/services/credential-route-context.d.ts +10 -0
  21. package/runtime/connectors/services/credential-route-context.js +19 -0
  22. package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +7 -1
  23. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +23 -0
  24. package/runtime/core/second-nature/orchestrator/effect-dispatcher.d.ts +3 -11
  25. package/runtime/core/second-nature/orchestrator/effect-dispatcher.js +9 -4
  26. package/runtime/core/second-nature/runtime/service-entry.js +1 -1
  27. package/runtime/observability/services/observability-retention.d.ts +10 -0
  28. package/runtime/observability/services/observability-retention.js +37 -0
  29. package/workspace-ops-bridge.js +11 -2
package/index.js CHANGED
@@ -96,6 +96,13 @@ const WORKSPACE_BRIDGE_COMMANDS = new Set([
96
96
  "heartbeat_check",
97
97
  "fallback",
98
98
  "storage_smoke",
99
+ // T1.2.8 (SN-CODE-03): capability probe surface via workspace bridge
100
+ "capability_probe",
101
+ // T1.2.6 / T1.2.7: policy show + audit read surface
102
+ "policy",
103
+ "audit",
104
+ // T3.3.2: near-real connector smoke sentinel
105
+ "near_real_smoke",
99
106
  ]);
100
107
  function isWorkspaceBridgeCommand(command, input) {
101
108
  if (command === "credential") {
@@ -153,18 +160,25 @@ function resolveWorkspaceRoot(toolWorkspaceRoot) {
153
160
  if (tool) {
154
161
  return { resolution: "tool_args", declaredRoot: tool, runtimeRoot: tool };
155
162
  }
156
- return { resolution: "unknown", declaredRoot: undefined, runtimeRoot: process.cwd() };
163
+ return {
164
+ resolution: "unknown",
165
+ declaredRoot: undefined,
166
+ runtimeRoot: process.cwd(),
167
+ };
157
168
  }
158
169
  function syncWorkspaceRootFromTool(spine, toolWorkspaceRoot) {
159
170
  const next = resolveWorkspaceRoot(toolWorkspaceRoot);
160
171
  const prev = spine.workspaceRootContext;
161
- const changed = next.runtimeRoot !== prev.runtimeRoot || next.resolution !== prev.resolution;
172
+ const changed = next.runtimeRoot !== prev.runtimeRoot ||
173
+ next.resolution !== prev.resolution;
162
174
  if (changed) {
163
175
  disposeWorkspaceOpsBridge();
164
176
  }
165
177
  spine.workspaceRootContext = next;
166
178
  if (changed) {
167
- spine.runtimeHandle = startRuntimeService({ workspaceRoot: next.runtimeRoot });
179
+ spine.runtimeHandle = startRuntimeService({
180
+ workspaceRoot: next.runtimeRoot,
181
+ });
168
182
  }
169
183
  }
170
184
  function trimRuntimeEvidence(spine) {
@@ -229,7 +243,8 @@ function parseExplainSubject(subjectRaw) {
229
243
  }
230
244
  function buildStatusPayload(spine) {
231
245
  const runtimeEvidence = latestRuntimeEvidence(spine);
232
- const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
246
+ const updatedAt = runtimeEvidence?.createdAt ??
247
+ new Date(spine.lifecycleState.lastChangedAt).toISOString();
233
248
  const wr = spine.workspaceRootContext;
234
249
  const needsRootHint = wr.resolution === "unknown";
235
250
  return {
@@ -240,7 +255,9 @@ function buildStatusPayload(spine) {
240
255
  error: {
241
256
  code: "WORKSPACE_READ_SURFACE_UNAVAILABLE",
242
257
  message: "Aggregated status requires workspace state; the host-safe plugin does not load persisted read models on this surface.",
243
- requiredUserInput: needsRootHint ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
258
+ requiredUserInput: needsRootHint
259
+ ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
260
+ : [],
244
261
  nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
245
262
  },
246
263
  data: {
@@ -264,7 +281,9 @@ function buildQuietPayload(spine, scope) {
264
281
  error: {
265
282
  code: "QUIET_READ_SURFACE_UNAVAILABLE",
266
283
  message: "Quiet read surface requires workspace runtime; not evaluated in host-safe carrier mode.",
267
- requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
284
+ requiredUserInput: wr.resolution === "unknown"
285
+ ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
286
+ : [],
268
287
  nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
269
288
  },
270
289
  data: {
@@ -285,7 +304,9 @@ function buildReportPayload(spine, day) {
285
304
  error: {
286
305
  code: "REPORT_READ_SURFACE_UNAVAILABLE",
287
306
  message: "Daily report artifacts require workspace runtime.",
288
- requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
307
+ requiredUserInput: wr.resolution === "unknown"
308
+ ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
309
+ : [],
289
310
  nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
290
311
  },
291
312
  data: {
@@ -317,7 +338,9 @@ function buildSessionPayload(spine, sessionId) {
317
338
  error: {
318
339
  code: "SESSION_READ_SURFACE_UNAVAILABLE",
319
340
  message: "Session analytics require workspace state database.",
320
- requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
341
+ requiredUserInput: wr.resolution === "unknown"
342
+ ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
343
+ : [],
321
344
  nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
322
345
  },
323
346
  data: {
@@ -338,7 +361,9 @@ function buildCredentialPayload(spine, platformId) {
338
361
  error: {
339
362
  code: "CREDENTIAL_READ_SURFACE_UNAVAILABLE",
340
363
  message: "Credential inspection requires workspace runtime on this surface.",
341
- requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
364
+ requiredUserInput: wr.resolution === "unknown"
365
+ ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
366
+ : [],
342
367
  nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
343
368
  },
344
369
  data: {
@@ -384,7 +409,9 @@ function buildExplainPayload(spine, subjectRaw) {
384
409
  error: {
385
410
  code: "EXPLAIN_READ_SURFACE_UNAVAILABLE",
386
411
  message: "Evidence-backed explain requires persisted workspace read models; host-safe carrier did not evaluate operator explain (CH-11-02).",
387
- requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
412
+ requiredUserInput: wr.resolution === "unknown"
413
+ ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
414
+ : [],
388
415
  nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
389
416
  },
390
417
  data: {
@@ -398,8 +425,13 @@ async function buildStorageSmokePayload(input) {
398
425
  try {
399
426
  const mod = await import("./runtime/storage/bootstrap/storage-mode-smoke.js");
400
427
  const runRepairFixture = Boolean(input?.runRepairFixture);
401
- const workspaceRoot = typeof input?.workspaceRoot === "string" ? input.workspaceRoot : undefined;
402
- const data = await mod.runStorageModeSmoke({ runRepairFixture, workspaceRoot });
428
+ const workspaceRoot = typeof input?.workspaceRoot === "string"
429
+ ? input.workspaceRoot
430
+ : undefined;
431
+ const data = await mod.runStorageModeSmoke({
432
+ runRepairFixture,
433
+ workspaceRoot,
434
+ });
403
435
  return { ok: true, data };
404
436
  }
405
437
  catch (error) {
@@ -434,8 +466,11 @@ function isProbeOnlyInput(input) {
434
466
  }
435
467
  function buildHeartbeatCheckPayload(spine, input) {
436
468
  const runtimeEvidence = latestRuntimeEvidence(spine);
437
- const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
438
- const timestamp = typeof input?.timestamp === "string" && input.timestamp.trim().length > 0 ? input.timestamp : updatedAt;
469
+ const updatedAt = runtimeEvidence?.createdAt ??
470
+ new Date(spine.lifecycleState.lastChangedAt).toISOString();
471
+ const timestamp = typeof input?.timestamp === "string" && input.timestamp.trim().length > 0
472
+ ? input.timestamp
473
+ : updatedAt;
439
474
  const wr = spine.workspaceRootContext;
440
475
  if (isProbeOnlyInput(input)) {
441
476
  return {
@@ -461,8 +496,10 @@ function buildHeartbeatCheckPayload(spine, input) {
461
496
  bridge: {
462
497
  timestamp,
463
498
  probeOnly: true,
464
- sessionContextProvided: typeof input?.sessionContext === "string" && input.sessionContext.trim().length > 0,
465
- heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" && input.heartbeatChecklist.trim().length > 0,
499
+ sessionContextProvided: typeof input?.sessionContext === "string" &&
500
+ input.sessionContext.trim().length > 0,
501
+ heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" &&
502
+ input.heartbeatChecklist.trim().length > 0,
466
503
  serviceEntryMode: "capability_probe",
467
504
  },
468
505
  },
@@ -491,19 +528,16 @@ function buildHeartbeatCheckPayload(spine, input) {
491
528
  },
492
529
  bridge: {
493
530
  timestamp,
494
- sessionContextProvided: typeof input?.sessionContext === "string" && input.sessionContext.trim().length > 0,
495
- heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" && input.heartbeatChecklist.trim().length > 0,
531
+ sessionContextProvided: typeof input?.sessionContext === "string" &&
532
+ input.sessionContext.trim().length > 0,
533
+ heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" &&
534
+ input.heartbeatChecklist.trim().length > 0,
496
535
  serviceEntryMode: "runtime_carrier_only",
497
536
  },
498
537
  },
499
538
  };
500
539
  }
501
540
  function createHostSafeRouter(spine) {
502
- const notImplemented = async (command) => ({
503
- ok: false,
504
- command,
505
- message: HOST_SAFE_LIMITATION_MESSAGE,
506
- });
507
541
  const commands = [
508
542
  {
509
543
  name: "status",
@@ -518,7 +552,7 @@ function createHostSafeRouter(spine) {
518
552
  if (action === "set") {
519
553
  return createUnavailableActionError("HOST_SAFE_POLICY_SET_UNAVAILABLE", "policy set is unavailable in the host-safe plugin package", ["social_daily_limit", "quiet_enabled"], "run_workspace_runtime_or_reinstall_full_build");
520
554
  }
521
- return notImplemented("policy");
555
+ return createUnavailableActionError("HOST_SAFE_POLICY_SHOW_UNAVAILABLE", "Policy read requires workspace state database; host-safe plugin does not load persisted policy rows.", [], "run_workspace_second_nature_cli_or_full_runtime_package");
522
556
  },
523
557
  },
524
558
  {
@@ -560,7 +594,7 @@ function createHostSafeRouter(spine) {
560
594
  {
561
595
  name: "audit",
562
596
  description: "Inspect audit and evidence views",
563
- execute: async () => notImplemented("audit"),
597
+ execute: async () => createUnavailableActionError("HOST_SAFE_AUDIT_UNAVAILABLE", "Audit read requires workspace observability database; host-safe plugin does not load persisted audit events.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
564
598
  },
565
599
  {
566
600
  name: "explain",
@@ -588,6 +622,16 @@ function createHostSafeRouter(spine) {
588
622
  description: "T4.1.4 storage mode smoke report (sql.js vs native probe)",
589
623
  execute: async (input) => buildStorageSmokePayload(input),
590
624
  },
625
+ {
626
+ name: "capability_probe",
627
+ description: "Probe host capabilities (workspace runtime required for full report)",
628
+ execute: async () => createUnavailableActionError("HOST_SAFE_CAPABILITY_PROBE_UNAVAILABLE", "Full capability probe requires workspace observability database for persistence; host-safe carrier returns static unknown.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
629
+ },
630
+ {
631
+ name: "near_real_smoke",
632
+ description: "Run near-real connector smoke (workspace runtime + connectors required)",
633
+ execute: async () => createUnavailableActionError("HOST_SAFE_NEAR_REAL_SMOKE_UNAVAILABLE", "Near-real connector smoke requires workspace state and observability databases; host-safe plugin cannot run connector harness.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
634
+ },
591
635
  ];
592
636
  return {
593
637
  commands,
@@ -600,7 +644,9 @@ function createActivationSpine() {
600
644
  const workspaceRootContext = resolveWorkspaceRoot(undefined);
601
645
  const spine = {
602
646
  router: undefined,
603
- runtimeHandle: startRuntimeService({ workspaceRoot: workspaceRootContext.runtimeRoot }),
647
+ runtimeHandle: startRuntimeService({
648
+ workspaceRoot: workspaceRootContext.runtimeRoot,
649
+ }),
604
650
  lifecycleState: getLifecycleState(),
605
651
  serviceStartRecorded: false,
606
652
  runtimeEvidence: [],
@@ -640,12 +686,15 @@ function refreshRegistrationState() {
640
686
  const spine = ensureActivationSpine();
641
687
  const workspaceRootContext = resolveWorkspaceRoot(undefined);
642
688
  const prev = spine.workspaceRootContext;
643
- const changed = workspaceRootContext.runtimeRoot !== prev.runtimeRoot || workspaceRootContext.resolution !== prev.resolution;
689
+ const changed = workspaceRootContext.runtimeRoot !== prev.runtimeRoot ||
690
+ workspaceRootContext.resolution !== prev.resolution;
644
691
  if (changed) {
645
692
  disposeWorkspaceOpsBridge();
646
693
  }
647
694
  spine.workspaceRootContext = workspaceRootContext;
648
- spine.runtimeHandle = startRuntimeService({ workspaceRoot: workspaceRootContext.runtimeRoot });
695
+ spine.runtimeHandle = startRuntimeService({
696
+ workspaceRoot: workspaceRootContext.runtimeRoot,
697
+ });
649
698
  spine.lifecycleState = recordRegistration();
650
699
  spine.serviceStartRecorded = false;
651
700
  recordRuntimeEvidence(spine, "register");
@@ -809,7 +858,11 @@ export default definePluginEntry({
809
858
  const resolved = spine.router.resolve(parsed.command);
810
859
  if (!resolved) {
811
860
  return {
812
- text: JSON.stringify({ ok: false, command: parsed.command, message: "Unknown Second Nature command." }),
861
+ text: JSON.stringify({
862
+ ok: false,
863
+ command: parsed.command,
864
+ message: "Unknown Second Nature command.",
865
+ }),
813
866
  };
814
867
  }
815
868
  const result = await routeSecondNatureCommand(spine, parsed.command, parsed.input);
@@ -827,7 +880,10 @@ export default definePluginEntry({
827
880
  content: [
828
881
  {
829
882
  type: "text",
830
- text: JSON.stringify({ ok: false, message: "Unknown Second Nature command." }),
883
+ text: JSON.stringify({
884
+ ok: false,
885
+ message: "Unknown Second Nature command.",
886
+ }),
831
887
  },
832
888
  ],
833
889
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.1.19",
4
+ "version": "0.1.21",
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 (see README / T1.1.4 ops norm).",
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.1.19",
3
+ "version": "0.1.21",
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",
@@ -1,7 +1,7 @@
1
1
  import { credentialVerify } from "./credential.js";
2
2
  import { formatExplanation } from "../explain/format-explanation.js";
3
3
  import { explainSurfaceSubject } from "../explain/explain-surface-subject.js";
4
- import { showOperatorFallback, OperatorFallbackNotFoundError } from "../ops/show-operator-fallback.js";
4
+ import { showOperatorFallback, OperatorFallbackNotFoundError, } from "../ops/show-operator-fallback.js";
5
5
  import { runStorageModeSmoke } from "../../storage/bootstrap/storage-mode-smoke.js";
6
6
  import { policySet } from "./policy.js";
7
7
  const notImplemented = async (command) => ({
@@ -40,7 +40,10 @@ export function createCliCommands(deps) {
40
40
  if (action === "set") {
41
41
  return policySet(actionBridge, input);
42
42
  }
43
- return notImplemented("policy");
43
+ // T1.2.6 (SN-CODE-01): `policy show` (default) returns the current rhythm policy
44
+ // snapshot. Returns workspace defaults when no policy row has been persisted yet.
45
+ const data = await readModels.loadPolicy();
46
+ return { ok: true, data };
44
47
  },
45
48
  },
46
49
  {
@@ -69,7 +72,9 @@ export function createCliCommands(deps) {
69
72
  name: "report",
70
73
  description: "Show daily report artifacts",
71
74
  execute: async (input) => {
72
- const day = typeof input?.day === "string" ? input.day : new Date().toISOString().slice(0, 10);
75
+ const day = typeof input?.day === "string"
76
+ ? input.day
77
+ : new Date().toISOString().slice(0, 10);
73
78
  const data = await readModels.loadDailyReport(day);
74
79
  return { ok: true, data };
75
80
  },
@@ -97,7 +102,12 @@ export function createCliCommands(deps) {
97
102
  {
98
103
  name: "audit",
99
104
  description: "Inspect audit and evidence views",
100
- execute: () => notImplemented("audit"),
105
+ execute: async () => {
106
+ // T1.2.7 (SN-CODE-02): minimal read-side view — list all in-memory audit events.
107
+ // Empty store returns { totalEvents: 0, events: [] } (honest empty, not an error).
108
+ const data = await readModels.loadAuditSummary();
109
+ return { ok: true, data };
110
+ },
101
111
  },
102
112
  {
103
113
  name: "explain",
@@ -148,8 +158,13 @@ export function createCliCommands(deps) {
148
158
  description: "T4.1.4 — report sql.js vs native SQLite probe and optional artifact→index repair fixture",
149
159
  execute: async (input) => {
150
160
  const runRepairFixture = Boolean(input?.runRepairFixture);
151
- const workspaceRoot = typeof input?.workspaceRoot === "string" ? input.workspaceRoot : undefined;
152
- const data = await runStorageModeSmoke({ runRepairFixture, workspaceRoot });
161
+ const workspaceRoot = typeof input?.workspaceRoot === "string"
162
+ ? input.workspaceRoot
163
+ : undefined;
164
+ const data = await runStorageModeSmoke({
165
+ runRepairFixture,
166
+ workspaceRoot,
167
+ });
153
168
  return { ok: true, data };
154
169
  },
155
170
  },
@@ -189,5 +204,21 @@ export function createCliCommands(deps) {
189
204
  }
190
205
  },
191
206
  },
207
+ {
208
+ name: "capability_probe",
209
+ description: "T1.2.8 — probe host capabilities and persist report (static unknown adapter in CLI context)",
210
+ execute: async (input) => {
211
+ const surface = await Promise.resolve(opsRouter.dispatch("capability_probe", input));
212
+ return surface;
213
+ },
214
+ },
215
+ {
216
+ name: "near_real_smoke",
217
+ description: "T3.3.2 — run near-real connector smoke (sentinel Moltbook + EvoMap, no live HTTP)",
218
+ execute: async (input) => {
219
+ const surface = await Promise.resolve(opsRouter.dispatch("near_real_smoke", input));
220
+ return surface;
221
+ },
222
+ },
192
223
  ];
193
224
  }
@@ -7,6 +7,7 @@ import { createOpsRouter } from "./ops/ops-router.js";
7
7
  import { createCliReadModels, } from "./read-models/index.js";
8
8
  import { resolvePackagedRuntime } from "./runtime/runtime-artifact-boundary.js";
9
9
  import { createRuntimeDecisionRecorder, } from "../observability/services/runtime-decision-recorder.js";
10
+ import { createConnectorExecutorAdapter } from "../connectors/services/connector-executor-adapter.js";
10
11
  export function createCliRuntimeDeps(overrides = {}) {
11
12
  const stateDb = overrides.stateDb ?? createStateDatabase();
12
13
  const observabilityDb = overrides.observabilityDb ?? createObservabilityDatabase();
@@ -16,6 +17,7 @@ export function createCliRuntimeDeps(overrides = {}) {
16
17
  stateDb,
17
18
  observabilityDb,
18
19
  workspaceRoot: process.cwd(),
20
+ livedExperienceAuditStore: overrides.livedExperienceAuditStore,
19
21
  });
20
22
  const actionBridge = overrides.actionBridge ?? createActionBridge(stateApi);
21
23
  const runtimeRecorder = overrides.runtimeRecorder ?? createRuntimeDecisionRecorder(observabilityDb);
@@ -31,12 +33,18 @@ export function createCliRuntimeDeps(overrides = {}) {
31
33
  export function createCommandRouter(options = {}) {
32
34
  const runtime = createCliRuntimeDeps(options.deps);
33
35
  const pluginRoot = path.join(process.cwd(), "plugin");
36
+ const connectorExecutor = createConnectorExecutorAdapter({
37
+ stateDb: runtime.stateDb,
38
+ observabilityDb: runtime.observabilityDb,
39
+ });
34
40
  const opsRouter = createOpsRouter({
35
41
  runtimeAvailable: resolvePackagedRuntime(pluginRoot).ok,
36
42
  readModels: runtime.readModels,
37
43
  runtimeRecorder: runtime.runtimeRecorder,
38
44
  state: runtime.stateDb,
39
45
  workspaceRoot: process.cwd(),
46
+ observabilityDb: runtime.observabilityDb,
47
+ connectorExecutor,
40
48
  });
41
49
  const commands = createCliCommands({
42
50
  readModels: runtime.readModels,
@@ -9,6 +9,7 @@ import type { HeartbeatSignal } from "../../core/second-nature/heartbeat/signal.
9
9
  import type { CliReadModels } from "../read-models/index.js";
10
10
  import type { RuntimeDecisionRecorder } from "../../observability/services/runtime-decision-recorder.js";
11
11
  import type { StateDatabase } from "../../storage/db/index.js";
12
+ import type { ConnectorExecutor } from "../../core/second-nature/orchestrator/effect-dispatcher.js";
12
13
  export type HeartbeatSurfaceStatus = "heartbeat_ok" | "intent_selected" | "denied" | "deferred" | "runtime_carrier_only" | "delivery_unavailable";
13
14
  export interface HeartbeatSurfaceResult {
14
15
  ok: boolean;
@@ -41,5 +42,10 @@ export interface HeartbeatCheckInput {
41
42
  timestamp?: string;
42
43
  sessionContext?: string;
43
44
  scopeHint?: HeartbeatSignal["scopeHint"];
45
+ /**
46
+ * When present, guard-allowed connector_action intents are dispatched through the
47
+ * connector-system instead of returning connector_dispatch_unwired.
48
+ */
49
+ connectorExecutor?: ConnectorExecutor;
44
50
  }
45
51
  export declare function heartbeatCheck(input: HeartbeatCheckInput): Promise<HeartbeatSurfaceResult>;
@@ -73,6 +73,7 @@ export async function heartbeatCheck(input) {
73
73
  runtimeRecorder: input.runtimeRecorder,
74
74
  state: input.state,
75
75
  workspaceRoot: input.workspaceRoot ?? process.cwd(),
76
+ connectorExecutor: input.connectorExecutor,
76
77
  });
77
78
  const cycle = await run(signal);
78
79
  return mapCycleToSurface(cycle, "workspace_full_runtime");
@@ -5,6 +5,8 @@ import { type HeartbeatCheckInput, type HeartbeatSurfaceResult } from "./heartbe
5
5
  import type { CliReadModels } from "../read-models/index.js";
6
6
  import type { RuntimeDecisionRecorder } from "../../observability/services/runtime-decision-recorder.js";
7
7
  import type { StateDatabase } from "../../storage/db/index.js";
8
+ import type { ObservabilityDatabase } from "../../observability/db/index.js";
9
+ import type { ConnectorExecutor } from "../../core/second-nature/orchestrator/effect-dispatcher.js";
8
10
  export interface OpsRouterDeps {
9
11
  /** When true, packaged runtime artifacts resolved and full graph is loadable */
10
12
  runtimeAvailable: boolean;
@@ -18,6 +20,16 @@ export interface OpsRouterDeps {
18
20
  */
19
21
  state?: StateDatabase;
20
22
  workspaceRoot?: string;
23
+ /**
24
+ * T1.2.8 (SN-CODE-03): observability DB for persisting capability probe reports.
25
+ * When absent, `capability_probe` still runs but skips persistence.
26
+ */
27
+ observabilityDb?: ObservabilityDatabase;
28
+ /**
29
+ * When present, guard-allowed connector_action intents are dispatched through the
30
+ * connector-system instead of returning connector_dispatch_unwired.
31
+ */
32
+ connectorExecutor?: ConnectorExecutor;
21
33
  }
22
34
  export interface OpsRouter {
23
35
  heartbeatCheck(input: HeartbeatCheckInput): Promise<HeartbeatSurfaceResult>;
@@ -3,10 +3,35 @@
3
3
  */
4
4
  import { heartbeatCheck, } from "./heartbeat-surface.js";
5
5
  import { showOperatorFallback, OperatorFallbackNotFoundError, } from "./show-operator-fallback.js";
6
+ import { probeHostCapability } from "../host-capability/probe-host-capability.js";
7
+ import { recordHostCapability } from "../host-capability/record-host-capability.js";
8
+ import { runNearRealConnectorSmoke } from "../../connectors/near-real/near-real-connector-smoke.js";
6
9
  function coerceProbeOnlyFlag(input) {
7
10
  const v = input?.probeOnly;
8
11
  return v === true || v === "true" || v === 1 || v === "1";
9
12
  }
13
+ /**
14
+ * T1.2.8 — static local adapter: all checks return `unknown` when no real host is available.
15
+ * Allows `capability_probe` to be called from CLI / workspace bridge without requiring a live host.
16
+ */
17
+ function createStaticUnknownAdapter() {
18
+ const now = new Date().toISOString();
19
+ const unknownResult = (name) => ({
20
+ name,
21
+ verdict: "unknown",
22
+ observedAt: now,
23
+ reason: "static_local_probe_no_host_context",
24
+ evidenceRefs: [],
25
+ });
26
+ return {
27
+ checkPluginLoad: () => unknownResult("plugin_load"),
28
+ checkHeartbeatBridge: () => unknownResult("heartbeat_bridge"),
29
+ checkHeartbeatToolInvocation: () => unknownResult("heartbeat_tool_invocation"),
30
+ checkDeliveryTarget: () => ({ status: "unknown", evidenceRefs: [] }),
31
+ checkAckDropBehavior: () => unknownResult("ack_drop"),
32
+ checkHookSupport: () => [],
33
+ };
34
+ }
10
35
  export function createOpsRouter(deps) {
11
36
  return {
12
37
  heartbeatCheck: (input) => heartbeatCheck({
@@ -16,6 +41,7 @@ export function createOpsRouter(deps) {
16
41
  runtimeRecorder: input.runtimeRecorder ?? deps.runtimeRecorder,
17
42
  state: input.state ?? deps.state,
18
43
  workspaceRoot: input.workspaceRoot ?? deps.workspaceRoot,
44
+ connectorExecutor: input.connectorExecutor ?? deps.connectorExecutor,
19
45
  }),
20
46
  dispatch(command, input) {
21
47
  if (command === "heartbeat_check") {
@@ -42,6 +68,8 @@ export function createOpsRouter(deps) {
42
68
  ? input.sessionContext
43
69
  : undefined,
44
70
  scopeHint: input?.scopeHint,
71
+ connectorExecutor: input
72
+ ?.connectorExecutor ?? deps.connectorExecutor,
45
73
  });
46
74
  }
47
75
  if (command === "fallback") {
@@ -90,6 +118,67 @@ export function createOpsRouter(deps) {
90
118
  }
91
119
  })();
92
120
  }
121
+ if (command === "capability_probe") {
122
+ // T1.2.8 (SN-CODE-03): run host capability probe with static unknown adapter (CLI context).
123
+ // Persists report when observabilityDb is available; returns safe JSON subset.
124
+ return (async () => {
125
+ const adapter = createStaticUnknownAdapter();
126
+ const docCheckedAt = new Date().toISOString();
127
+ const report = probeHostCapability({
128
+ adapter,
129
+ docLinks: [],
130
+ docCheckedAt,
131
+ });
132
+ if (deps.observabilityDb) {
133
+ await recordHostCapability(deps.observabilityDb, report);
134
+ }
135
+ return {
136
+ ok: true,
137
+ command: "capability_probe",
138
+ data: {
139
+ reportId: report.reportId,
140
+ generatedAt: report.generatedAt,
141
+ deliveryTarget: report.deliveryTarget,
142
+ pluginLoad: { verdict: report.pluginLoad.verdict },
143
+ heartbeatBridge: { verdict: report.heartbeatBridge.verdict },
144
+ heartbeatToolInvocation: {
145
+ verdict: report.heartbeatToolInvocation.verdict,
146
+ },
147
+ ackDropBehavior: { verdict: report.ackDropBehavior.verdict },
148
+ conflictCount: report.conflictRecords.length,
149
+ recommendedNextStep: report.recommendedNextStep,
150
+ note: "static_local_probe: all verdicts are unknown without live host context",
151
+ },
152
+ };
153
+ })();
154
+ }
155
+ if (command === "near_real_smoke") {
156
+ // T3.3.2 (SN-CODE-05): wrap runNearRealConnectorSmoke as an ops surface command.
157
+ // Requires state + observabilityDb + workspaceRoot to be wired into OpsRouterDeps.
158
+ if (!deps.state || !deps.observabilityDb || !deps.workspaceRoot) {
159
+ return {
160
+ ok: false,
161
+ command: "near_real_smoke",
162
+ error: {
163
+ code: "NEAR_REAL_SMOKE_DEPS_UNAVAILABLE",
164
+ message: "near_real_smoke requires state, observabilityDb, and workspaceRoot in OpsRouterDeps",
165
+ nextStep: "wire_deps_into_ops_router",
166
+ },
167
+ };
168
+ }
169
+ return (async () => {
170
+ const result = await runNearRealConnectorSmoke({
171
+ state: deps.state,
172
+ observabilityDb: deps.observabilityDb,
173
+ workspaceRoot: deps.workspaceRoot,
174
+ });
175
+ return {
176
+ ok: true,
177
+ command: "near_real_smoke",
178
+ data: result,
179
+ };
180
+ })();
181
+ }
93
182
  return {
94
183
  ok: false,
95
184
  error: {
@@ -17,6 +17,7 @@ import type { SnapshotInputs } from "../../core/second-nature/heartbeat/snapshot
17
17
  import type { CliReadModels } from "../read-models/index.js";
18
18
  import type { RuntimeDecisionRecorder } from "../../observability/services/runtime-decision-recorder.js";
19
19
  import type { StateDatabase } from "../../storage/db/index.js";
20
+ import type { ConnectorExecutor } from "../../core/second-nature/orchestrator/effect-dispatcher.js";
20
21
  export interface WorkspaceHeartbeatRunnerOptions {
21
22
  /** When supplied, the runner persists the cycle so `loadStatus` can read it (T1.2.3). */
22
23
  runtimeRecorder?: RuntimeDecisionRecorder;
@@ -32,6 +33,11 @@ export interface WorkspaceHeartbeatRunnerOptions {
32
33
  * Defaults to true when workspaceRoot is provided, since this is the host-safe workspace path.
33
34
  */
34
35
  enableQuietWorkflow?: boolean;
36
+ /**
37
+ * When present, guard-allowed connector_action intents are dispatched through the
38
+ * connector-system instead of returning connector_dispatch_unwired.
39
+ */
40
+ connectorExecutor?: ConnectorExecutor;
35
41
  }
36
42
  export declare function loadSnapshotInputsForWorkspaceHeartbeat(readModels: CliReadModels, options?: {
37
43
  state?: StateDatabase;
@@ -76,6 +76,7 @@ export function createWorkspaceHeartbeatRunner(readModels, options = {}) {
76
76
  quietWorkflow: quietEnabled
77
77
  ? { workspaceRoot: options.workspaceRoot }
78
78
  : undefined,
79
+ connectorExecutor: options.connectorExecutor,
79
80
  },
80
81
  });
81
82
  if (options.runtimeRecorder) {
@@ -1,9 +1,12 @@
1
1
  import type { StateDatabase } from "../../storage/db/index.js";
2
2
  import type { ObservabilityDatabase } from "../../observability/db/index.js";
3
3
  import { AppendOnlyAuditStore } from "../../observability/audit/append-only-audit-store.js";
4
- import type { StatusReadModel, DailyReportReadModel, QuietReadModel, SessionDetailReadModel, CredentialReadModel, ExplainReadModel, ExplainSubjectKind } from "./types.js";
4
+ import type { StatusReadModel, DailyReportReadModel, QuietReadModel, SessionDetailReadModel, CredentialReadModel, ExplainReadModel, ExplainSubjectKind, AuditSummaryReadModel } from "./types.js";
5
+ export type { AuditSummaryReadModel } from "./types.js";
5
6
  export type { ExplainSubjectKind } from "./types.js";
6
7
  import type { OperatorFallbackView } from "../../storage/fallback/operator-fallback-view.js";
8
+ import { type RhythmPolicySnapshot } from "../../storage/rhythm/rhythm-policy-snapshot.js";
9
+ export type { RhythmPolicySnapshot };
7
10
  export interface CliReadModels {
8
11
  loadStatus(scope?: string): Promise<StatusReadModel>;
9
12
  loadDailyReport(day: string): Promise<DailyReportReadModel>;
@@ -13,6 +16,14 @@ export interface CliReadModels {
13
16
  explain(subject: ExplainSubject): Promise<ExplainReadModel>;
14
17
  /** T1.2.2 — persisted operator fallback; view status is always not_sent. */
15
18
  loadFallbackView(ref: string): Promise<OperatorFallbackView | null>;
19
+ /** T1.2.6 — rhythm policy snapshot for operator `policy show`. */
20
+ loadPolicy(): Promise<RhythmPolicySnapshot>;
21
+ /**
22
+ * T1.2.7 (SN-CODE-02) — minimal audit read-side view for operator `audit` command.
23
+ * Returns a summary of all in-memory audit events in the default store.
24
+ * Empty store returns `{ totalEvents: 0, events: [] }` (honest empty, not an error).
25
+ */
26
+ loadAuditSummary(): Promise<AuditSummaryReadModel>;
16
27
  }
17
28
  /** T1.2.1 / T1.2.2 — operator-facing read surface (subset of full CLI read models). */
18
29
  export type OpsReadModelPort = Pick<CliReadModels, "loadStatus" | "loadDailyReport" | "loadQuiet" | "loadSession" | "loadCredential" | "explain" | "loadFallbackView">;
@@ -10,6 +10,7 @@ import { AppendOnlyAuditStore } from "../../observability/audit/append-only-audi
10
10
  import { queryExplain, } from "../../observability/query/explain-query.js";
11
11
  import { mapOperatorExplainToReadModel } from "./operator-explain-map.js";
12
12
  import { loadOperatorFallbackRow, toOperatorFallbackView, } from "../../storage/fallback/load-operator-fallback.js";
13
+ import { loadRhythmPolicySnapshot, } from "../../storage/rhythm/rhythm-policy-snapshot.js";
13
14
  const INTERNAL_RUNTIME_PLATFORM_ID = "second-nature-runtime";
14
15
  const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
15
16
  function toExplainQuery(subject) {
@@ -92,6 +93,11 @@ function mapRuntimeStatus(attempt) {
92
93
  if (!attempt) {
93
94
  return "unknown";
94
95
  }
96
+ // T1.2.9 (SN-CODE-04): control-plane denial (no eligible intent) is NOT a runtime fault.
97
+ // Return `awaiting_sources` so operators do not misread a clean denied cycle as a crash/degraded.
98
+ if (attempt.failureClass === "decision_denied") {
99
+ return "awaiting_sources";
100
+ }
95
101
  if (attempt.failureClass || attempt.status === "failed") {
96
102
  return "degraded";
97
103
  }
@@ -324,6 +330,26 @@ export function createCliReadModels(deps) {
324
330
  return null;
325
331
  return toOperatorFallbackView(row);
326
332
  },
333
+ // T1.2.6 (SN-CODE-01): return the current workspace rhythm policy snapshot so that
334
+ // `policy show` is no longer a notImplemented shell. Returns defaults if no policy row exists.
335
+ async loadPolicy() {
336
+ return loadRhythmPolicySnapshot(deps.stateDb);
337
+ },
338
+ // T1.2.7 (SN-CODE-02): minimal audit read-side for operator `audit` command.
339
+ // Lists all in-memory envelopes with safe redacted fields; empty store returns honest empty.
340
+ async loadAuditSummary() {
341
+ const events = auditStore.list();
342
+ return {
343
+ totalEvents: events.length,
344
+ events: events.map((e) => ({
345
+ eventId: e.eventId,
346
+ family: e.family,
347
+ plane: e.plane,
348
+ createdAt: e.createdAt,
349
+ sensitivity: e.redaction.sensitivity,
350
+ })),
351
+ };
352
+ },
327
353
  async explain(subject) {
328
354
  const q = toExplainQuery(subject);
329
355
  // T1.2.5: auditStore is always non-null (default-injected), so the explain path always
@@ -1,6 +1,11 @@
1
1
  export interface RuntimeSummary {
2
2
  host: "openclaw-plugin";
3
- serviceStatus: "idle" | "running" | "degraded" | "unknown";
3
+ /**
4
+ * T1.2.9 (SN-CODE-04): `awaiting_sources` signals that the last runtime cycle was
5
+ * control-plane denied (decision_denied) — no eligible intent found, NOT a delivery
6
+ * or execution fault. Operators must not interpret this as a runtime crash.
7
+ */
8
+ serviceStatus: "idle" | "running" | "degraded" | "awaiting_sources" | "unknown";
4
9
  updatedAt: string;
5
10
  }
6
11
  export interface RhythmSummary {
@@ -110,3 +115,15 @@ export interface ExplainReadModel {
110
115
  warnings?: string[];
111
116
  relatedAuditEventIds?: string[];
112
117
  }
118
+ /** T1.2.7 (SN-CODE-02) — minimal audit read-side summary for operator `audit` command. */
119
+ export interface AuditEventSummaryEntry {
120
+ eventId: string;
121
+ family: string;
122
+ plane: string;
123
+ createdAt: string;
124
+ sensitivity: string;
125
+ }
126
+ export interface AuditSummaryReadModel {
127
+ totalEvents: number;
128
+ events: AuditEventSummaryEntry[];
129
+ }
@@ -78,6 +78,16 @@ export interface ExecutionRunner {
78
78
  export interface ConnectorExecutionPort {
79
79
  executeCapability(intent: CapabilityIntent, request: ConnectorRequest): Promise<ConnectorResult<unknown>>;
80
80
  }
81
+ export interface ConnectorExecutor {
82
+ executeEffect(input: {
83
+ platformId: string;
84
+ intent: CapabilityIntent;
85
+ payload: Record<string, unknown>;
86
+ decisionId: string;
87
+ intentId: string;
88
+ idempotencyKey: string;
89
+ }): Promise<ConnectorResult<unknown>>;
90
+ }
81
91
  export declare function normalizeOutcome(attempt: RawAttempt): ConnectorResult<unknown>;
82
92
  export declare function createConnectorContractCore(input: {
83
93
  manifestLoader: ConnectorManifestLoader;
@@ -1,6 +1,14 @@
1
1
  import { z } from "zod";
2
- import { classifyFailure, ConnectorPolicyError } from "./failure-taxonomy.js";
3
- export const CHANNEL_TYPES = ["api_rest", "api_rpc", "a2a", "mcp", "cli", "skill", "browser"];
2
+ import { classifyFailure, ConnectorPolicyError, } from "./failure-taxonomy.js";
3
+ export const CHANNEL_TYPES = [
4
+ "api_rest",
5
+ "api_rpc",
6
+ "a2a",
7
+ "mcp",
8
+ "cli",
9
+ "skill",
10
+ "browser",
11
+ ];
4
12
  export const CAPABILITY_INTENTS = [
5
13
  "feed.read",
6
14
  "post.publish",
@@ -40,11 +40,15 @@ export class ConnectorPolicyError extends Error {
40
40
  }
41
41
  function readRetryAfterMs(input) {
42
42
  const retryAfterMs = input.retryAfterMs;
43
- if (typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
43
+ if (typeof retryAfterMs === "number" &&
44
+ Number.isFinite(retryAfterMs) &&
45
+ retryAfterMs > 0) {
44
46
  return retryAfterMs;
45
47
  }
46
48
  const retryAfterSeconds = input.retryAfterSeconds;
47
- if (typeof retryAfterSeconds === "number" && Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
49
+ if (typeof retryAfterSeconds === "number" &&
50
+ Number.isFinite(retryAfterSeconds) &&
51
+ retryAfterSeconds > 0) {
48
52
  return retryAfterSeconds * 1000;
49
53
  }
50
54
  return undefined;
@@ -64,24 +68,56 @@ export function classifyFailure(error) {
64
68
  const record = error;
65
69
  const code = record.code;
66
70
  if (typeof code === "string") {
71
+ if (code === "auth_failure")
72
+ return {
73
+ class: "auth_failure",
74
+ retryable: RETRYABLE_BY_CLASS.auth_failure,
75
+ };
67
76
  if (code === "verification_required")
68
- return { class: "verification_required", retryable: RETRYABLE_BY_CLASS.verification_required };
77
+ return {
78
+ class: "verification_required",
79
+ retryable: RETRYABLE_BY_CLASS.verification_required,
80
+ };
69
81
  if (code === "credential_expired")
70
- return { class: "credential_expired", retryable: RETRYABLE_BY_CLASS.credential_expired };
82
+ return {
83
+ class: "credential_expired",
84
+ retryable: RETRYABLE_BY_CLASS.credential_expired,
85
+ };
71
86
  if (code === "cooldown_blocked")
72
- return { class: "cooldown_blocked", retryable: RETRYABLE_BY_CLASS.cooldown_blocked };
87
+ return {
88
+ class: "cooldown_blocked",
89
+ retryable: RETRYABLE_BY_CLASS.cooldown_blocked,
90
+ };
73
91
  if (code === "idempotency_conflict")
74
- return { class: "idempotency_conflict", retryable: RETRYABLE_BY_CLASS.idempotency_conflict };
92
+ return {
93
+ class: "idempotency_conflict",
94
+ retryable: RETRYABLE_BY_CLASS.idempotency_conflict,
95
+ };
75
96
  if (code === "concurrency_conflict")
76
- return { class: "concurrency_conflict", retryable: RETRYABLE_BY_CLASS.concurrency_conflict };
97
+ return {
98
+ class: "concurrency_conflict",
99
+ retryable: RETRYABLE_BY_CLASS.concurrency_conflict,
100
+ };
77
101
  if (code === "protocol_mismatch")
78
- return { class: "protocol_mismatch", retryable: RETRYABLE_BY_CLASS.protocol_mismatch };
102
+ return {
103
+ class: "protocol_mismatch",
104
+ retryable: RETRYABLE_BY_CLASS.protocol_mismatch,
105
+ };
79
106
  if (code === "semantic_rejection")
80
- return { class: "semantic_rejection", retryable: RETRYABLE_BY_CLASS.semantic_rejection };
107
+ return {
108
+ class: "semantic_rejection",
109
+ retryable: RETRYABLE_BY_CLASS.semantic_rejection,
110
+ };
81
111
  if (code === "transport_failure")
82
- return { class: "transport_failure", retryable: RETRYABLE_BY_CLASS.transport_failure };
112
+ return {
113
+ class: "transport_failure",
114
+ retryable: RETRYABLE_BY_CLASS.transport_failure,
115
+ };
83
116
  if (code === "permanent_input_error")
84
- return { class: "permanent_input_error", retryable: RETRYABLE_BY_CLASS.permanent_input_error };
117
+ return {
118
+ class: "permanent_input_error",
119
+ retryable: RETRYABLE_BY_CLASS.permanent_input_error,
120
+ };
85
121
  }
86
122
  const status = record.status;
87
123
  if (status === 429) {
@@ -92,14 +128,26 @@ export function classifyFailure(error) {
92
128
  };
93
129
  }
94
130
  if (status === 401 || status === 403) {
95
- return { class: "auth_failure", retryable: RETRYABLE_BY_CLASS.auth_failure };
131
+ return {
132
+ class: "auth_failure",
133
+ retryable: RETRYABLE_BY_CLASS.auth_failure,
134
+ };
96
135
  }
97
136
  if (status === 400 || status === 404 || status === 422) {
98
- return { class: "permanent_input_error", retryable: RETRYABLE_BY_CLASS.permanent_input_error };
137
+ return {
138
+ class: "permanent_input_error",
139
+ retryable: RETRYABLE_BY_CLASS.permanent_input_error,
140
+ };
99
141
  }
100
142
  if (status === 500 || status === 502 || status === 503 || status === 504) {
101
- return { class: "transport_failure", retryable: RETRYABLE_BY_CLASS.transport_failure };
143
+ return {
144
+ class: "transport_failure",
145
+ retryable: RETRYABLE_BY_CLASS.transport_failure,
146
+ };
102
147
  }
103
148
  }
104
- return { class: "unknown_platform_change", retryable: RETRYABLE_BY_CLASS.unknown_platform_change };
149
+ return {
150
+ class: "unknown_platform_change",
151
+ retryable: RETRYABLE_BY_CLASS.unknown_platform_change,
152
+ };
105
153
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Adapter: assemble connector-system execution infrastructure into the
3
+ * ConnectorExecutor interface consumed by EffectDispatcher.
4
+ *
5
+ * When credentials / base URLs are missing, returns an honest
6
+ * terminal_failure instead of throwing so the heartbeat loop stays stable.
7
+ */
8
+ import type { ConnectorExecutor } from "../base/contract.js";
9
+ export type { ConnectorExecutor } from "../base/contract.js";
10
+ import type { ObservabilityDatabase } from "../../observability/db/index.js";
11
+ import type { StateDatabase } from "../../storage/db/index.js";
12
+ export interface ConnectorExecutorAdapterOptions {
13
+ stateDb: StateDatabase;
14
+ observabilityDb: ObservabilityDatabase;
15
+ }
16
+ export declare function createConnectorExecutorAdapter(options: ConnectorExecutorAdapterOptions): ConnectorExecutor;
@@ -0,0 +1,118 @@
1
+ import { CapabilityContractRegistry } from "../base/manifest.js";
2
+ import { ConnectorRoutePlanner } from "../base/route-planner.js";
3
+ import { ChannelHealthStore } from "../base/channel-health.js";
4
+ import { createConnectorPolicyLayer } from "../base/policy-layer.js";
5
+ import { InMemoryEffectCommitLedger } from "../base/execution-policy.js";
6
+ import { moltbookManifest } from "../social-community/moltbook/manifest.js";
7
+ import { evomapManifest } from "../agent-network/evomap/manifest.js";
8
+ import { createMoltbookApiClient } from "../social-community/moltbook/api-client.js";
9
+ import { createMoltbookRunner } from "../social-community/moltbook/adapter.js";
10
+ import { ExecutionTelemetry } from "../../observability/services/execution-telemetry.js";
11
+ import { createCredentialVault } from "../../storage/services/credential-vault.js";
12
+ import { createCredentialRouteContextPort } from "./credential-route-context.js";
13
+ function createAdaptiveExecutionRunner(vault) {
14
+ return {
15
+ async run(_plan, request) {
16
+ const platformId = request.platformId;
17
+ const started = Date.now();
18
+ const credential = await vault.loadCredentialContext(platformId);
19
+ if (!credential ||
20
+ credential.status !== "active" ||
21
+ !credential.encryptedValue) {
22
+ return {
23
+ platformId,
24
+ channel: request.preferredChannel ?? "api_rest",
25
+ latencyMs: Date.now() - started,
26
+ success: false,
27
+ error: {
28
+ code: "auth_failure",
29
+ detail: "credential_unavailable_for_execution",
30
+ },
31
+ };
32
+ }
33
+ if (platformId === "moltbook") {
34
+ const baseUrl = process.env.SECOND_NATURE_MOLTBOOK_BASE_URL;
35
+ if (!baseUrl) {
36
+ return {
37
+ platformId,
38
+ channel: request.preferredChannel ?? "api_rest",
39
+ latencyMs: Date.now() - started,
40
+ success: false,
41
+ error: {
42
+ code: "configuration_missing",
43
+ detail: "SECOND_NATURE_MOLTBOOK_BASE_URL not set",
44
+ },
45
+ };
46
+ }
47
+ const apiClient = createMoltbookApiClient({
48
+ baseUrl,
49
+ accessToken: credential.encryptedValue,
50
+ timeoutMs: 10000,
51
+ });
52
+ const runner = createMoltbookRunner({
53
+ apiClient,
54
+ skillRunner: {
55
+ run: async () => {
56
+ throw {
57
+ code: "protocol_mismatch",
58
+ detail: "moltbook_skill_runner_not_configured",
59
+ };
60
+ },
61
+ },
62
+ });
63
+ return runner.run(_plan, request);
64
+ }
65
+ if (platformId === "evomap") {
66
+ return {
67
+ platformId,
68
+ channel: request.preferredChannel ?? "api_rest",
69
+ latencyMs: Date.now() - started,
70
+ success: false,
71
+ error: {
72
+ code: "not_implemented",
73
+ detail: "evomap_execution_runner_not_yet_implemented",
74
+ },
75
+ };
76
+ }
77
+ return {
78
+ platformId,
79
+ channel: request.preferredChannel ?? "api_rest",
80
+ latencyMs: Date.now() - started,
81
+ success: false,
82
+ error: {
83
+ code: "unknown_platform",
84
+ detail: `no execution runner for ${platformId}`,
85
+ },
86
+ };
87
+ },
88
+ };
89
+ }
90
+ export function createConnectorExecutorAdapter(options) {
91
+ const vault = createCredentialVault(options.stateDb.db);
92
+ const registry = new CapabilityContractRegistry();
93
+ registry.register({ ...moltbookManifest });
94
+ registry.register({ ...evomapManifest });
95
+ const routeContextPort = createCredentialRouteContextPort(vault);
96
+ const routePlanner = new ConnectorRoutePlanner(registry, routeContextPort, new ChannelHealthStore());
97
+ const telemetry = new ExecutionTelemetry(options.observabilityDb);
98
+ const executionRunner = createAdaptiveExecutionRunner(vault);
99
+ const policy = createConnectorPolicyLayer({
100
+ routePlanner,
101
+ executionRunner,
102
+ telemetry,
103
+ effectCommitLedger: new InMemoryEffectCommitLedger(),
104
+ retryPolicy: { maxRetries: 2, jitter: true },
105
+ });
106
+ return {
107
+ async executeEffect(input) {
108
+ return policy.executeWithPolicy(input.intent, {
109
+ platformId: input.platformId,
110
+ intent: input.intent,
111
+ payload: input.payload,
112
+ decisionId: input.decisionId,
113
+ intentId: input.intentId,
114
+ idempotencyKey: input.idempotencyKey,
115
+ });
116
+ },
117
+ };
118
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Bridge CredentialVault → RouteContextPort for connector route planning.
3
+ *
4
+ * Loads decrypted credentials from state DB and maps them to the
5
+ * CredentialContext shape expected by ConnectorRoutePlanner.
6
+ * Cooldown is stubbed (always unblocked) until a cooldown ledger is modeled.
7
+ */
8
+ import type { RouteContextPort } from "../base/contract.js";
9
+ import type { CredentialVault } from "../../storage/services/credential-vault.js";
10
+ export declare function createCredentialRouteContextPort(vault: CredentialVault): RouteContextPort;
@@ -0,0 +1,19 @@
1
+ export function createCredentialRouteContextPort(vault) {
2
+ return {
3
+ async loadCredentialState(platformId) {
4
+ const ctx = await vault.loadCredentialContext(platformId);
5
+ // Defensive: some ORM findFirst variants return {} instead of null/undefined.
6
+ if (!ctx || !ctx.platformId || !ctx.status) {
7
+ return {
8
+ platformId,
9
+ status: "missing",
10
+ credentialType: "api_key",
11
+ };
12
+ }
13
+ return ctx;
14
+ },
15
+ async loadCooldownState() {
16
+ return { blocked: false };
17
+ },
18
+ };
19
+ }
@@ -19,6 +19,7 @@ import { type HeartbeatRuntimeSnapshot } from "./runtime-snapshot.js";
19
19
  import type { GuidanceDraftPort } from "../../../guidance/outreach-draft-schema.js";
20
20
  import type { StateDatabase } from "../../../storage/db/index.js";
21
21
  import { type OpenClawDeliveryPort } from "../outreach/dispatch-user-outreach.js";
22
+ import type { ConnectorExecutor } from "../../../connectors/base/contract.js";
22
23
  export interface HeartbeatDecisionTracePayload {
23
24
  scope: RuntimeScope;
24
25
  status: HeartbeatCycleStatus;
@@ -44,7 +45,7 @@ export interface HeartbeatQuietWorkflowDeps {
44
45
  * Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
45
46
  * Exported for unit tests (CR-M1 wiring).
46
47
  */
47
- export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow">): Promise<HeartbeatCycleResult>;
48
+ export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow" | "connectorExecutor">): Promise<HeartbeatCycleResult>;
48
49
  export interface HeartbeatDeps {
49
50
  /** Load snapshot inputs from state-system */
50
51
  loadSnapshotInputs: () => Promise<SnapshotInputs>;
@@ -52,6 +53,11 @@ export interface HeartbeatDeps {
52
53
  recordDecisionTrace?: (payload: HeartbeatDecisionTracePayload) => Promise<void>;
53
54
  outreachDispatch?: HeartbeatOutreachDispatchDeps;
54
55
  quietWorkflow?: HeartbeatQuietWorkflowDeps;
56
+ /**
57
+ * When present, guard-allowed connector_action intents are dispatched
58
+ * through the connector-system instead of returning connector_dispatch_unwired.
59
+ */
60
+ connectorExecutor?: ConnectorExecutor;
55
61
  }
56
62
  /**
57
63
  * Ingest a heartbeat rhythm signal and drive one full decision round.
@@ -5,6 +5,7 @@ import { evaluateHardGuards } from "../orchestrator/guard-layer.js";
5
5
  import { dispatchUserOutreachIntent, } from "../outreach/dispatch-user-outreach.js";
6
6
  import { buildJudgeOutreachInputFromSnapshot } from "../outreach/judge-input-from-snapshot.js";
7
7
  import { runSourceBackedQuiet } from "../quiet/run-source-backed-quiet.js";
8
+ import { toCapabilityIntent } from "../orchestrator/effect-dispatcher.js";
8
9
  /**
9
10
  * Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
10
11
  * Exported for unit tests (CR-M1 wiring).
@@ -48,6 +49,28 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
48
49
  intent.effectClass === "no_effect" ||
49
50
  intent.kind === "maintenance";
50
51
  const connectorUnwired = intent.effectClass === "connector_action";
52
+ if (connectorUnwired && deps.connectorExecutor) {
53
+ const result = await deps.connectorExecutor.executeEffect({
54
+ platformId: intent.platformId ?? "unknown",
55
+ intent: toCapabilityIntent(intent),
56
+ payload: {},
57
+ decisionId: `decision:${intent.id}:${Date.now()}`,
58
+ intentId: intent.id,
59
+ idempotencyKey: `idem:${intent.id}:${Date.now()}`,
60
+ });
61
+ const base = {
62
+ scope: "rhythm",
63
+ status: "intent_selected",
64
+ selectedIntentId: intent.id,
65
+ decisionId: `decision:${intent.id}:${Date.now()}`,
66
+ reasons: result.status === "success"
67
+ ? ["connector_effect_executed"]
68
+ : result.status === "retryable_failure"
69
+ ? ["connector_retryable_failure", result.failureClass ?? "unknown"]
70
+ : ["connector_terminal_failure", result.failureClass ?? "unknown"],
71
+ };
72
+ return base;
73
+ }
51
74
  const reasons = noExternalEffect
52
75
  ? ["internal_tick"]
53
76
  : connectorUnwired
@@ -1,4 +1,5 @@
1
- import type { ConnectorResult, CapabilityIntent } from "../../../connectors/base/contract.js";
1
+ import type { ConnectorResult, CapabilityIntent, ConnectorExecutor } from "../../../connectors/base/contract.js";
2
+ export type { ConnectorExecutor } from "../../../connectors/base/contract.js";
2
3
  import { LeaseManager, type EffectClass } from "./lease-manager.js";
3
4
  export interface AllowedIntent {
4
5
  id: string;
@@ -31,16 +32,6 @@ export interface IntentCommitPort {
31
32
  }): Promise<void>;
32
33
  abortIntentCommit(id: string, reason: string): Promise<void>;
33
34
  }
34
- export interface ConnectorExecutor {
35
- executeEffect(input: {
36
- platformId: string;
37
- intent: CapabilityIntent;
38
- payload: Record<string, unknown>;
39
- decisionId: string;
40
- intentId: string;
41
- idempotencyKey: string;
42
- }): Promise<ConnectorResult<unknown>>;
43
- }
44
35
  export interface CheckpointPort {
45
36
  saveCheckpoint(input: {
46
37
  id: string;
@@ -83,6 +74,7 @@ export type DispatchResult = {
83
74
  status: "maintenance_done";
84
75
  commitId: string;
85
76
  };
77
+ export declare function toCapabilityIntent(intent: Pick<AllowedIntent, "kind">): CapabilityIntent;
86
78
  export declare class EffectDispatcher {
87
79
  private readonly leaseManager;
88
80
  private readonly commitPort;
@@ -1,14 +1,17 @@
1
1
  import * as crypto from "crypto";
2
2
  function needsLease(effectClass) {
3
- return effectClass === "external_platform_action" || effectClass === "connector_action" || effectClass === "user_outreach";
3
+ return (effectClass === "external_platform_action" ||
4
+ effectClass === "connector_action" ||
5
+ effectClass === "user_outreach");
4
6
  }
5
7
  function needsCheckpoint(effectClass) {
6
8
  return effectClass !== "maintenance" && effectClass !== "no_effect";
7
9
  }
8
10
  function isConnectorEffect(effectClass) {
9
- return effectClass === "external_platform_action" || effectClass === "connector_action";
11
+ return (effectClass === "external_platform_action" ||
12
+ effectClass === "connector_action");
10
13
  }
11
- function toCapabilityIntent(intent) {
14
+ export function toCapabilityIntent(intent) {
12
15
  if (intent.kind === "work")
13
16
  return "work.discover";
14
17
  if (intent.kind === "exploration")
@@ -48,7 +51,9 @@ export class EffectDispatcher {
48
51
  id: decision.checkpointId,
49
52
  tickId: decision.tickId,
50
53
  intentId: decision.intentId,
51
- phase: isConnectorEffect(intent.effectClass) ? "before_effect" : "before_quiet_write",
54
+ phase: isConnectorEffect(intent.effectClass)
55
+ ? "before_effect"
56
+ : "before_quiet_write",
52
57
  snapshotRef: decision.traceId,
53
58
  });
54
59
  }
@@ -27,7 +27,7 @@ export function startRuntimeService(ctx) {
27
27
  // - control-plane-system (heartbeat bridge preparation)
28
28
  const workspaceRoot = ctx?.workspaceRoot ?? process.cwd();
29
29
  /** Keep in sync with `plugin/package.json` when cutting releases. */
30
- const version = "0.1.19";
30
+ const version = "0.1.20";
31
31
  activeHandle = {
32
32
  ready: true,
33
33
  version,
@@ -0,0 +1,10 @@
1
+ import type { ObservabilityDatabase } from "../db/index.js";
2
+ export interface RetentionCleanupInput {
3
+ /** Delete rows with createdAt < this ISO string. */
4
+ beforeDate: string;
5
+ }
6
+ export interface RetentionCleanupResult {
7
+ decisionLedgerDeleted: number;
8
+ executionAttemptsDeleted: number;
9
+ }
10
+ export declare function pruneObservabilityTables(db: ObservabilityDatabase, input: RetentionCleanupInput): Promise<RetentionCleanupResult>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Observability retention cleanup (P2-06).
3
+ *
4
+ * Core logic: delete rows older than a retention threshold from
5
+ * decision_ledger and execution_attempts. Host capability reports and
6
+ * governance audit are intentionally kept longer (they are rare and
7
+ * operator-relevant).
8
+ *
9
+ * Boundaries:
10
+ * - Does NOT vacuum the SQLite file (callers may do so separately).
11
+ * - Returns honest counts so operators can verify.
12
+ * - Safe to run while the system is active (SQLite DELETE is row-level).
13
+ */
14
+ import { lt, sql } from "drizzle-orm";
15
+ import { decisionLedger, executionAttempts } from "../db/schema/index.js";
16
+ export async function pruneObservabilityTables(db, input) {
17
+ // Count before delete so we can return honest deletion numbers
18
+ // (SQLite DELETE result does not expose changes in Drizzle's type).
19
+ const dlBefore = await db.db
20
+ .select({ count: sql `count(*)` })
21
+ .from(decisionLedger)
22
+ .where(lt(decisionLedger.createdAt, input.beforeDate));
23
+ const eaBefore = await db.db
24
+ .select({ count: sql `count(*)` })
25
+ .from(executionAttempts)
26
+ .where(lt(executionAttempts.startedAt, input.beforeDate));
27
+ await db.db
28
+ .delete(decisionLedger)
29
+ .where(lt(decisionLedger.createdAt, input.beforeDate));
30
+ await db.db
31
+ .delete(executionAttempts)
32
+ .where(lt(executionAttempts.startedAt, input.beforeDate));
33
+ return {
34
+ decisionLedgerDeleted: dlBefore[0]?.count ?? 0,
35
+ executionAttemptsDeleted: eaBefore[0]?.count ?? 0,
36
+ };
37
+ }
@@ -22,7 +22,9 @@ export async function openWorkspaceOpsBridge(workspaceRoot) {
22
22
  try {
23
23
  const pluginPackageRoot = path.dirname(fileURLToPath(import.meta.url));
24
24
  // Packaged `plugin/runtime` is emitted JS without sibling `.d.ts` in this repo layout.
25
- // @ts-expect-error TS7016intentional dynamic import of artifact bundle
25
+ // Dynamic import of artifact bundle typed via PackagedCliModule interface above.
26
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
27
+ // @ts-ignore TS7016 — intentional: runtime artifact has no adjacent .d.ts in this layout
26
28
  const cliIndex = (await import("./runtime/cli/index.js"));
27
29
  const commandsMod = (await import("./runtime/cli/commands/index.js"));
28
30
  const storageDb = (await import("./runtime/storage/db/index.js"));
@@ -40,6 +42,10 @@ export async function openWorkspaceOpsBridge(workspaceRoot) {
40
42
  runtimeAvailable: runtimeResolved.ok,
41
43
  readModels: deps.readModels,
42
44
  runtimeRecorder: deps.runtimeRecorder,
45
+ // T1.2.8 (SN-CODE-03): pass observabilityDb so capability_probe can persist reports
46
+ observabilityDb,
47
+ state: stateDb,
48
+ workspaceRoot: resolvedRoot,
43
49
  });
44
50
  const commands = commandsMod.createCliCommands({
45
51
  readModels: deps.readModels,
@@ -51,7 +57,10 @@ export async function openWorkspaceOpsBridge(workspaceRoot) {
51
57
  if (!def) {
52
58
  return {
53
59
  ok: false,
54
- error: { code: "unknown_command", message: `Unknown Second Nature command: ${command}` },
60
+ error: {
61
+ code: "unknown_command",
62
+ message: `Unknown Second Nature command: ${command}`,
63
+ },
55
64
  };
56
65
  }
57
66
  const prevCwd = process.cwd();