@haaaiawd/second-nature 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -16,15 +16,111 @@
16
16
  * - structured mutating flows such as policy set / credential verify remain unavailable here
17
17
  * - full evidence-backed workspace runtime can be reintroduced later behind a host-safe boundary
18
18
  *
19
+ * OpenClaw operator norm (T1.1.4 / T1.1.5): set `SECOND_NATURE_WORKSPACE_ROOT` or tool `workspaceRoot` to the
20
+ * **same absolute path** as the OpenClaw **agent workspace** (default `~/.openclaw/workspace`, or
21
+ * `agents.defaults.workspace` in `~/.openclaw/openclaw.json`). Do **not** infer that root from the plugin
22
+ * install directory. With **sandbox** or **per-agent workspaces**, use the path where `data/state.db` and
23
+ * `workspace/` anchors actually live. See `explore/reports/2026-05-04_openclaw-plugin-install-vs-workspace-root.md`.
24
+ *
19
25
  * Test coverage:
20
26
  * - tests/integration/cli/plugin-runtime-registration.test.ts
21
27
  * - tests/integration/cli/plugin-packaging-walkthrough.test.ts
28
+ * - tests/integration/cli/plugin-workspace-ops-bridge.test.ts (T1.1.4 / CH-13 matrix, T1.1.5 ops docs cross-ref)
22
29
  */
23
30
  import { startRuntimeService, } from "./runtime/core/second-nature/runtime/service-entry.js";
24
31
  import { getLifecycleState, recordRegistration, } from "./runtime/core/second-nature/runtime/lifecycle-service.js";
32
+ import { openWorkspaceOpsBridge } from "./workspace-ops-bridge.js";
25
33
  const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
26
34
  const HOST_SAFE_LIMITATION_MESSAGE = "Host-safe plugin package keeps synchronous register/load semantics, but mutating workspace runtime flows remain unavailable here.";
27
35
  let activationSpine = null;
36
+ /** T1.1.4 — lazily opened full read bridge; closed when workspace root / resolution changes. */
37
+ let workspaceOpsBridge = null;
38
+ function disposeWorkspaceOpsBridge() {
39
+ if (workspaceOpsBridge) {
40
+ workspaceOpsBridge.close();
41
+ workspaceOpsBridge = null;
42
+ }
43
+ }
44
+ const WORKSPACE_BRIDGE_COMMANDS = new Set([
45
+ "status",
46
+ "quiet",
47
+ "report",
48
+ "session",
49
+ "explain",
50
+ "heartbeat_check",
51
+ "fallback",
52
+ "storage_smoke",
53
+ ]);
54
+ function isWorkspaceBridgeCommand(command, input) {
55
+ if (command === "credential") {
56
+ const action = typeof input?.action === "string" ? input.action : "show";
57
+ return action !== "verify";
58
+ }
59
+ return WORKSPACE_BRIDGE_COMMANDS.has(command);
60
+ }
61
+ async function ensureWorkspaceOpsBridge(spine) {
62
+ const root = spine.workspaceRootContext.runtimeRoot;
63
+ if (workspaceOpsBridge?.root === root) {
64
+ return { ok: true, dispatch: workspaceOpsBridge.dispatch };
65
+ }
66
+ disposeWorkspaceOpsBridge();
67
+ const opened = await openWorkspaceOpsBridge(root);
68
+ if (!opened.ok) {
69
+ return opened;
70
+ }
71
+ workspaceOpsBridge = { root, close: opened.close, dispatch: opened.dispatch };
72
+ return { ok: true, dispatch: opened.dispatch };
73
+ }
74
+ async function routeSecondNatureCommand(spine, command, input) {
75
+ const wr = spine.workspaceRootContext;
76
+ const useBridge = wr.resolution !== "unknown" && isWorkspaceBridgeCommand(command, input);
77
+ if (useBridge) {
78
+ const bridge = await ensureWorkspaceOpsBridge(spine);
79
+ if (!bridge.ok) {
80
+ return {
81
+ ok: false,
82
+ surfaceMode: "host_safe_carrier",
83
+ workspaceReadModelsEvaluated: false,
84
+ message: HOST_SAFE_LIMITATION_MESSAGE,
85
+ error: bridge.error,
86
+ data: {
87
+ workspaceRootResolution: wr.resolution,
88
+ bridgeAttempted: true,
89
+ declaredRoot: wr.declaredRoot,
90
+ },
91
+ };
92
+ }
93
+ return (await bridge.dispatch(command, input));
94
+ }
95
+ const def = spine.router.resolve(command);
96
+ if (!def) {
97
+ return { ok: false, message: `Unknown Second Nature command: ${command}` };
98
+ }
99
+ return def.execute(input);
100
+ }
101
+ function resolveWorkspaceRoot(toolWorkspaceRoot) {
102
+ const env = process.env.SECOND_NATURE_WORKSPACE_ROOT?.trim();
103
+ if (env) {
104
+ return { resolution: "env", declaredRoot: env, runtimeRoot: env };
105
+ }
106
+ const tool = toolWorkspaceRoot?.trim();
107
+ if (tool) {
108
+ return { resolution: "tool_args", declaredRoot: tool, runtimeRoot: tool };
109
+ }
110
+ return { resolution: "unknown", declaredRoot: undefined, runtimeRoot: process.cwd() };
111
+ }
112
+ function syncWorkspaceRootFromTool(spine, toolWorkspaceRoot) {
113
+ const next = resolveWorkspaceRoot(toolWorkspaceRoot);
114
+ const prev = spine.workspaceRootContext;
115
+ const changed = next.runtimeRoot !== prev.runtimeRoot || next.resolution !== prev.resolution;
116
+ if (changed) {
117
+ disposeWorkspaceOpsBridge();
118
+ }
119
+ spine.workspaceRootContext = next;
120
+ if (changed) {
121
+ spine.runtimeHandle = startRuntimeService({ workspaceRoot: next.runtimeRoot });
122
+ }
123
+ }
28
124
  function trimRuntimeEvidence(spine) {
29
125
  if (spine.runtimeEvidence.length > 12) {
30
126
  spine.runtimeEvidence.splice(0, spine.runtimeEvidence.length - 12);
@@ -88,56 +184,73 @@ function parseExplainSubject(subjectRaw) {
88
184
  function buildStatusPayload(spine) {
89
185
  const runtimeEvidence = latestRuntimeEvidence(spine);
90
186
  const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
187
+ const wr = spine.workspaceRootContext;
188
+ const needsRootHint = wr.resolution === "unknown";
91
189
  return {
92
- ok: true,
190
+ ok: false,
191
+ surfaceMode: "host_safe_carrier",
192
+ workspaceReadModelsEvaluated: false,
193
+ message: HOST_SAFE_LIMITATION_MESSAGE,
194
+ error: {
195
+ code: "WORKSPACE_READ_SURFACE_UNAVAILABLE",
196
+ message: "Aggregated status requires workspace state; the host-safe plugin does not load persisted read models on this surface.",
197
+ requiredUserInput: needsRootHint ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
198
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
199
+ },
93
200
  data: {
94
- runtime: {
201
+ workspaceRootResolution: wr.resolution,
202
+ carrier: {
95
203
  host: "openclaw-plugin",
96
204
  serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
97
205
  updatedAt,
98
- },
99
- rhythm: {
100
- mode: "active",
101
- windowId: undefined,
102
- },
103
- quiet: {
104
- mode: "unknown",
105
- lastEvent: runtimeEvidence?.traceId,
106
- interrupted: undefined,
107
- },
108
- connectors: [],
109
- credentials: [],
110
- risk: {
111
- level: "low",
112
- flags: [],
206
+ lastRuntimeTraceId: runtimeEvidence?.traceId,
113
207
  },
114
208
  },
115
209
  };
116
210
  }
117
- function buildQuietPayload(scope) {
211
+ function buildQuietPayload(spine, scope) {
212
+ const wr = spine.workspaceRootContext;
118
213
  return {
119
- ok: true,
214
+ ok: false,
215
+ surfaceMode: "host_safe_carrier",
216
+ workspaceReadModelsEvaluated: false,
217
+ message: HOST_SAFE_LIMITATION_MESSAGE,
218
+ error: {
219
+ code: "QUIET_READ_SURFACE_UNAVAILABLE",
220
+ message: "Quiet read surface requires workspace runtime; not evaluated in host-safe carrier mode.",
221
+ requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
222
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
223
+ },
120
224
  data: {
121
225
  scope,
122
- mode: "unknown",
123
- sourceCount: 0,
124
- reportCount: 0,
125
- recentJournalCount: 0,
226
+ evaluated: false,
227
+ unavailableReason: "host_safe_carrier_no_workspace_db",
228
+ workspaceRootResolution: wr.resolution,
126
229
  },
127
230
  };
128
231
  }
129
- function buildReportPayload(day) {
232
+ function buildReportPayload(spine, day) {
233
+ const wr = spine.workspaceRootContext;
130
234
  return {
131
- ok: true,
235
+ ok: false,
236
+ surfaceMode: "host_safe_carrier",
237
+ workspaceReadModelsEvaluated: false,
238
+ message: HOST_SAFE_LIMITATION_MESSAGE,
239
+ error: {
240
+ code: "REPORT_READ_SURFACE_UNAVAILABLE",
241
+ message: "Daily report artifacts require workspace runtime.",
242
+ requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
243
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
244
+ },
132
245
  data: {
246
+ evaluated: false,
247
+ unavailableReason: "host_safe_carrier_no_workspace_db",
133
248
  day: day && day.trim() ? day : new Date().toISOString().slice(0, 10),
134
- summary: "",
135
- highlights: [],
136
- sourceRefs: [],
249
+ workspaceRootResolution: wr.resolution,
137
250
  },
138
251
  };
139
252
  }
140
- function buildSessionPayload(sessionId) {
253
+ function buildSessionPayload(spine, sessionId) {
141
254
  if (!sessionId) {
142
255
  return {
143
256
  ok: false,
@@ -149,26 +262,44 @@ function buildSessionPayload(sessionId) {
149
262
  },
150
263
  };
151
264
  }
265
+ const wr = spine.workspaceRootContext;
152
266
  return {
153
- ok: true,
267
+ ok: false,
268
+ surfaceMode: "host_safe_carrier",
269
+ workspaceReadModelsEvaluated: false,
270
+ message: HOST_SAFE_LIMITATION_MESSAGE,
271
+ error: {
272
+ code: "SESSION_READ_SURFACE_UNAVAILABLE",
273
+ message: "Session analytics require workspace state database.",
274
+ requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
275
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
276
+ },
154
277
  data: {
155
278
  requestedSessionId: sessionId,
156
- traceId: sessionId,
157
- decisionCount: 0,
158
- attemptCount: 0,
159
- governanceCount: 0,
160
- keyFactors: [],
161
- evidenceRefs: [],
279
+ evaluated: false,
280
+ unavailableReason: "host_safe_carrier_no_workspace_db",
281
+ workspaceRootResolution: wr.resolution,
162
282
  },
163
283
  };
164
284
  }
165
- function buildCredentialPayload(platformId) {
285
+ function buildCredentialPayload(spine, platformId) {
286
+ const wr = spine.workspaceRootContext;
166
287
  return {
167
- ok: true,
288
+ ok: false,
289
+ surfaceMode: "host_safe_carrier",
290
+ workspaceReadModelsEvaluated: false,
291
+ message: HOST_SAFE_LIMITATION_MESSAGE,
292
+ error: {
293
+ code: "CREDENTIAL_READ_SURFACE_UNAVAILABLE",
294
+ message: "Credential inspection requires workspace runtime on this surface.",
295
+ requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
296
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
297
+ },
168
298
  data: {
169
- platformId: platformId && platformId.trim() ? platformId : "unknown",
170
- status: "missing",
171
- nextStep: "provide_credential_context",
299
+ platformId: platformId && platformId.trim() ? platformId : undefined,
300
+ evaluated: false,
301
+ unavailableReason: "host_safe_carrier_no_workspace_db",
302
+ workspaceRootResolution: wr.resolution,
172
303
  },
173
304
  };
174
305
  }
@@ -198,23 +329,22 @@ function buildExplainPayload(spine, subjectRaw) {
198
329
  }
199
330
  return createUnavailableActionError("EXPLAIN_SUBJECT_INVALID", "invalid explain subject", ["subject"], "reinvoke_explain_with_supported_subject");
200
331
  }
201
- const runtimeEvidence = latestRuntimeEvidence(spine);
332
+ const wr = spine.workspaceRootContext;
202
333
  return {
203
- ok: true,
334
+ ok: false,
335
+ surfaceMode: "host_safe_carrier",
336
+ workspaceReadModelsEvaluated: false,
337
+ message: HOST_SAFE_LIMITATION_MESSAGE,
338
+ error: {
339
+ code: "EXPLAIN_READ_SURFACE_UNAVAILABLE",
340
+ message: "Evidence-backed explain requires persisted workspace read models; host-safe carrier did not evaluate operator explain (CH-11-02).",
341
+ requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
342
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
343
+ },
204
344
  data: {
205
345
  subjectType: subject.subjectType,
206
- conclusion: "Plugin surface is loaded in host-safe mode with a minimal activation spine.",
207
- keyFactors: [
208
- "synchronous_register",
209
- `subject:${subject.subjectId}`,
210
- runtimeEvidence?.capability ?? "runtime.activate",
211
- ],
212
- evidenceRefs: [
213
- runtimeEvidence?.traceId ?? `${INTERNAL_RUNTIME_TRACE_PREFIX}none`,
214
- `subject:${subjectRaw.trim()}`,
215
- "host_safe_mode",
216
- ],
217
- nextStep: "use full workspace runtime for evidence-backed explain details",
346
+ evaluated: false,
347
+ workspaceRootResolution: wr.resolution,
218
348
  },
219
349
  };
220
350
  }
@@ -252,20 +382,58 @@ function buildFallbackHostSafePayload(ref) {
252
382
  }
253
383
  return createUnavailableActionError("HOST_SAFE_FALLBACK_VIEW_UNAVAILABLE", "Operator fallback view requires workspace state database; host-safe plugin cannot read persisted fallback artifacts.", ["ref"], "run_workspace_second_nature_cli_or_full_runtime_package");
254
384
  }
385
+ function isProbeOnlyInput(input) {
386
+ const v = input?.probeOnly;
387
+ return v === true || v === "true" || v === 1 || v === "1";
388
+ }
255
389
  function buildHeartbeatCheckPayload(spine, input) {
256
390
  const runtimeEvidence = latestRuntimeEvidence(spine);
257
391
  const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
258
392
  const timestamp = typeof input?.timestamp === "string" && input.timestamp.trim().length > 0 ? input.timestamp : updatedAt;
393
+ const wr = spine.workspaceRootContext;
394
+ if (isProbeOnlyInput(input)) {
395
+ return {
396
+ ok: true,
397
+ status: "heartbeat_ok",
398
+ surfaceMode: "capability_probe",
399
+ reasons: ["probe_only"],
400
+ livedExperienceLoopClaimed: false,
401
+ scope: "rhythm",
402
+ trigger: "heartbeat_bridge",
403
+ message: "Capability probe only on the host-safe carrier surface; does not claim a full lived-experience decision loop.",
404
+ data: {
405
+ workspaceRootResolution: wr.resolution,
406
+ runtime: {
407
+ host: "openclaw-plugin",
408
+ serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
409
+ updatedAt,
410
+ },
411
+ surface: {
412
+ tool: "second_nature_ops",
413
+ command: "second-nature heartbeat_check",
414
+ },
415
+ bridge: {
416
+ timestamp,
417
+ probeOnly: true,
418
+ sessionContextProvided: typeof input?.sessionContext === "string" && input.sessionContext.trim().length > 0,
419
+ heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" && input.heartbeatChecklist.trim().length > 0,
420
+ serviceEntryMode: "capability_probe",
421
+ },
422
+ },
423
+ };
424
+ }
259
425
  return {
260
426
  ok: true,
261
- status: "heartbeat_ok",
262
- heartbeat: "HEARTBEAT_OK",
427
+ status: "runtime_carrier_only",
428
+ surfaceMode: "host_safe_carrier",
429
+ livedExperienceLoopClaimed: false,
263
430
  scope: "rhythm",
264
431
  trigger: "heartbeat_bridge",
265
- reasons: ["host_safe_bridge_ready"],
266
- nextAction: "continue",
267
- message: "Host-safe heartbeat bridge acknowledged the round. No additional action is required from this surface.",
432
+ reasons: ["runtime_carrier_only", "host_safe_bridge_ack"],
433
+ nextAction: "continue_carrier_surface_only",
434
+ message: "Packaged carrier acknowledged this heartbeat round. This is not a full lived-experience decision loop; use the workspace CLI when read models are required.",
268
435
  data: {
436
+ workspaceRootResolution: wr.resolution,
269
437
  runtime: {
270
438
  host: "openclaw-plugin",
271
439
  serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
@@ -316,7 +484,7 @@ function createHostSafeRouter(spine) {
316
484
  return createUnavailableActionError("HOST_SAFE_CREDENTIAL_VERIFY_UNAVAILABLE", "credential verify is unavailable in the host-safe plugin package", ["verification_answer"], "run_workspace_runtime_or_reinstall_full_build");
317
485
  }
318
486
  const platformId = typeof input?.platformId === "string" ? input.platformId : undefined;
319
- return buildCredentialPayload(platformId);
487
+ return buildCredentialPayload(spine, platformId);
320
488
  },
321
489
  },
322
490
  {
@@ -324,7 +492,7 @@ function createHostSafeRouter(spine) {
324
492
  description: "Inspect Quiet lifecycle state",
325
493
  execute: async (input) => {
326
494
  const scope = typeof input?.scope === "string" ? input.scope : undefined;
327
- return buildQuietPayload(scope);
495
+ return buildQuietPayload(spine, scope);
328
496
  },
329
497
  },
330
498
  {
@@ -332,7 +500,7 @@ function createHostSafeRouter(spine) {
332
500
  description: "Show daily report artifacts",
333
501
  execute: async (input) => {
334
502
  const day = typeof input?.day === "string" ? input.day : undefined;
335
- return buildReportPayload(day);
503
+ return buildReportPayload(spine, day);
336
504
  },
337
505
  },
338
506
  {
@@ -340,7 +508,7 @@ function createHostSafeRouter(spine) {
340
508
  description: "Inspect continuity session details",
341
509
  execute: async (input) => {
342
510
  const sessionId = typeof input?.sessionId === "string" ? input.sessionId : undefined;
343
- return buildSessionPayload(sessionId);
511
+ return buildSessionPayload(spine, sessionId);
344
512
  },
345
513
  },
346
514
  {
@@ -383,12 +551,14 @@ function createHostSafeRouter(spine) {
383
551
  };
384
552
  }
385
553
  function createActivationSpine() {
554
+ const workspaceRootContext = resolveWorkspaceRoot(undefined);
386
555
  const spine = {
387
556
  router: undefined,
388
- runtimeHandle: startRuntimeService({ workspaceRoot: process.cwd() }),
557
+ runtimeHandle: startRuntimeService({ workspaceRoot: workspaceRootContext.runtimeRoot }),
389
558
  lifecycleState: getLifecycleState(),
390
559
  serviceStartRecorded: false,
391
560
  runtimeEvidence: [],
561
+ workspaceRootContext,
392
562
  };
393
563
  spine.router = createHostSafeRouter(spine);
394
564
  return spine;
@@ -422,7 +592,14 @@ function recordRuntimeEvidence(spine, origin) {
422
592
  }
423
593
  function refreshRegistrationState() {
424
594
  const spine = ensureActivationSpine();
425
- spine.runtimeHandle = startRuntimeService({ workspaceRoot: process.cwd() });
595
+ const workspaceRootContext = resolveWorkspaceRoot(undefined);
596
+ const prev = spine.workspaceRootContext;
597
+ const changed = workspaceRootContext.runtimeRoot !== prev.runtimeRoot || workspaceRootContext.resolution !== prev.resolution;
598
+ if (changed) {
599
+ disposeWorkspaceOpsBridge();
600
+ }
601
+ spine.workspaceRootContext = workspaceRootContext;
602
+ spine.runtimeHandle = startRuntimeService({ workspaceRoot: workspaceRootContext.runtimeRoot });
426
603
  spine.lifecycleState = recordRegistration();
427
604
  spine.serviceStartRecorded = false;
428
605
  recordRuntimeEvidence(spine, "register");
@@ -575,7 +752,7 @@ export default {
575
752
  text: JSON.stringify({ ok: false, command: parsed.command, message: "Unknown Second Nature command." }),
576
753
  };
577
754
  }
578
- const result = await resolved.execute(parsed.input);
755
+ const result = await routeSecondNatureCommand(spine, parsed.command, parsed.input);
579
756
  return {
580
757
  text: JSON.stringify(result),
581
758
  };
@@ -590,11 +767,16 @@ export default {
590
767
  properties: {
591
768
  command: { type: "string" },
592
769
  args: { type: "object", additionalProperties: true },
770
+ workspaceRoot: {
771
+ type: "string",
772
+ description: "Workspace root for packaged smoke/runtime alignment (optional; prefer SECOND_NATURE_WORKSPACE_ROOT).",
773
+ },
593
774
  },
594
775
  required: ["command"],
595
776
  },
596
777
  async execute(_id, params) {
597
778
  const spine = ensureActivationSpine();
779
+ syncWorkspaceRootFromTool(spine, params.workspaceRoot);
598
780
  const resolved = spine.router.resolve(params.command);
599
781
  if (!resolved) {
600
782
  return {
@@ -606,7 +788,7 @@ export default {
606
788
  ],
607
789
  };
608
790
  }
609
- const result = await resolved.execute(params.args);
791
+ const result = await routeSecondNatureCommand(spine, params.command, params.args);
610
792
  return {
611
793
  content: [
612
794
  {
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.1.9",
4
+ "version": "0.1.11",
5
5
  "entry": "./index.js",
6
- "description": "OpenClaw native plugin package with synchronous surface registration and a bundled runtime spine.",
6
+ "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).",
7
7
  "capabilities": {
8
8
  "commands": [
9
9
  "second-nature"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/second-nature",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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",
@@ -17,6 +17,7 @@
17
17
  "main": "./index.js",
18
18
  "files": [
19
19
  "index.js",
20
+ "workspace-ops-bridge.js",
20
21
  "openclaw.plugin.json",
21
22
  "runtime/"
22
23
  ],
@@ -48,8 +48,8 @@ export async function heartbeatCheck(input) {
48
48
  if (!input.readModels) {
49
49
  return {
50
50
  ok: true,
51
- status: "heartbeat_ok",
52
- surfaceMode: "workspace_full_runtime",
51
+ status: "runtime_carrier_only",
52
+ surfaceMode: "host_safe_carrier",
53
53
  reasons: ["heartbeat_read_models_unavailable"],
54
54
  livedExperienceLoopClaimed: false,
55
55
  };
@@ -3,6 +3,10 @@
3
3
  */
4
4
  import { heartbeatCheck } from "./heartbeat-surface.js";
5
5
  import { showOperatorFallback, OperatorFallbackNotFoundError } from "./show-operator-fallback.js";
6
+ function coerceProbeOnlyFlag(input) {
7
+ const v = input?.probeOnly;
8
+ return v === true || v === "true" || v === 1 || v === "1";
9
+ }
6
10
  export function createOpsRouter(deps) {
7
11
  return {
8
12
  heartbeatCheck: (input) => heartbeatCheck({
@@ -14,12 +18,12 @@ export function createOpsRouter(deps) {
14
18
  if (command === "heartbeat_check") {
15
19
  const runtimeAvailable = typeof input?.runtimeAvailable === "boolean" ? input.runtimeAvailable : deps.runtimeAvailable;
16
20
  return heartbeatCheck({
17
- probeOnly: Boolean(input?.probeOnly),
21
+ probeOnly: coerceProbeOnlyFlag(input),
18
22
  runtimeAvailable,
19
23
  fakeControlPlanePassthrough: input?.fakeControlPlanePassthrough && typeof input.fakeControlPlanePassthrough === "object"
20
24
  ? input.fakeControlPlanePassthrough
21
25
  : undefined,
22
- readModels: deps.readModels,
26
+ readModels: input?.readModels ?? deps.readModels,
23
27
  timestamp: typeof input?.timestamp === "string" ? input.timestamp : undefined,
24
28
  sessionContext: typeof input?.sessionContext === "string" ? input.sessionContext : undefined,
25
29
  scopeHint: input?.scopeHint,
@@ -26,7 +26,8 @@ export function startRuntimeService(ctx) {
26
26
  // - observability-system (event store setup)
27
27
  // - control-plane-system (heartbeat bridge preparation)
28
28
  const workspaceRoot = ctx?.workspaceRoot ?? process.cwd();
29
- const version = "0.1.0";
29
+ /** Keep in sync with `plugin/package.json` when cutting releases. */
30
+ const version = "0.1.11";
30
31
  activeHandle = {
31
32
  ready: true,
32
33
  version,
@@ -59,8 +59,8 @@ export declare const sceneGuidanceRequestSchema: z.ZodObject<{
59
59
  maintenance: "maintenance";
60
60
  }>>;
61
61
  riskLevel: z.ZodEnum<{
62
- low: "low";
63
62
  medium: "medium";
63
+ low: "low";
64
64
  high: "high";
65
65
  }>;
66
66
  sourceRefs: z.ZodArray<z.ZodObject<{
@@ -113,8 +113,8 @@ export declare const outreachDraftRequestSchema: z.ZodObject<{
113
113
  maintenance: "maintenance";
114
114
  }>>;
115
115
  riskLevel: z.ZodEnum<{
116
- low: "low";
117
116
  medium: "medium";
117
+ low: "low";
118
118
  high: "high";
119
119
  }>;
120
120
  sourceRefs: z.ZodArray<z.ZodObject<{
@@ -185,7 +185,7 @@ export declare function parseOutreachDraftRequest(input: unknown): OutreachDraft
185
185
  export declare function safeParseOutreachDraftRequest(input: unknown): z.ZodSafeParseResult<{
186
186
  requestId: string;
187
187
  runtimeScope: "rhythm" | "user_reply" | "user_task";
188
- riskLevel: "low" | "medium" | "high";
188
+ riskLevel: "medium" | "low" | "high";
189
189
  sourceRefs: {
190
190
  id: string;
191
191
  kind: "platform_item" | "workspace_artifact" | "decision_record" | "user_anchor" | "connector_result" | "host_report" | "fallback_artifact";
@@ -91,7 +91,8 @@ export class LivedExperienceAuditRecorder {
91
91
  payload.status === "not_sent_fallback" ||
92
92
  payload.status === "channel_missing" ||
93
93
  payload.status === "host_unsupported" ||
94
- payload.status === "failed") {
94
+ payload.status === "failed" ||
95
+ payload.status === "ack_dropped") {
95
96
  entry.noUserVisibleContact = true;
96
97
  }
97
98
  return { eventId: envelope.eventId };
@@ -12,8 +12,19 @@ export class CredentialRepository {
12
12
  });
13
13
  }
14
14
  async findByPlatformId(platformId) {
15
- return this.database.db.query.credentialRecords.findFirst({
15
+ const row = await this.database.db.query.credentialRecords.findFirst({
16
16
  where: eq(credentialRecords.platformId, platformId),
17
17
  });
18
+ if (row == null) {
19
+ return undefined;
20
+ }
21
+ const r = row;
22
+ const pid = (r.platformId ?? r.platform_id);
23
+ const enc = (r.encryptedValue ?? r.encrypted_value);
24
+ // sql.js + Drizzle: no-match can still return a "shell" row (keys present, values undefined).
25
+ if (pid == null || pid === "" || enc == null || enc === "") {
26
+ return undefined;
27
+ }
28
+ return row;
18
29
  }
19
30
  }
@@ -90,9 +90,10 @@ export function createCredentialVault(db) {
90
90
  return null;
91
91
  let plain;
92
92
  if (record.encryptedValue) {
93
- plain = isCredentialCiphertext(record.encryptedValue)
94
- ? decryptCredentialAtRest(record.encryptedValue)
95
- : record.encryptedValue;
93
+ if (!isCredentialCiphertext(record.encryptedValue)) {
94
+ throw new Error("credential_store_plaintext_or_invalid_legacy_record");
95
+ }
96
+ plain = decryptCredentialAtRest(record.encryptedValue);
96
97
  }
97
98
  return {
98
99
  platformId: record.platformId,
@@ -0,0 +1,78 @@
1
+ /**
2
+ * T1.1.4 — Lazy workspace full-ops bridge (OpenClaw plugin).
3
+ *
4
+ * Core logic: dynamic-import packaged `runtime/` + open workspace `data/*.db` with the same
5
+ * `createCliRuntimeDeps` + `createOpsRouter` + `createCliCommands` path as the workspace CLI.
6
+ * `process.chdir(workspaceRoot)` during dispatch so `memory/workspace` paths match CLI cwd semantics.
7
+ *
8
+ * Boundaries: no static imports from `./runtime/*` (sql.js top-level await stays out of register() graph).
9
+ *
10
+ * Plan B (CH-11-01): if the host VM blocks dynamic import + sql.js, fall back to a subprocess invoking
11
+ * the workspace `second-nature` CLI — not implemented here; bridge failures surface as explicit errors.
12
+ *
13
+ * Test coverage: tests/integration/cli/plugin-workspace-ops-bridge.test.ts
14
+ */
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ const PLUGIN_PACKAGE_ROOT = path.dirname(fileURLToPath(import.meta.url));
19
+ export async function openWorkspaceOpsBridge(workspaceRoot) {
20
+ const resolvedRoot = path.resolve(workspaceRoot);
21
+ try {
22
+ // Packaged `plugin/runtime` is emitted JS without sibling `.d.ts` in this repo layout.
23
+ // @ts-expect-error TS7016 — intentional dynamic import of artifact bundle
24
+ const cliIndex = (await import("./runtime/cli/index.js"));
25
+ const commandsMod = (await import("./runtime/cli/commands/index.js"));
26
+ const storageDb = (await import("./runtime/storage/db/index.js"));
27
+ const obsDb = (await import("./runtime/observability/db/index.js"));
28
+ const boundary = (await import("./runtime/cli/runtime/runtime-artifact-boundary.js"));
29
+ const dataDir = path.join(resolvedRoot, "data");
30
+ fs.mkdirSync(dataDir, { recursive: true });
31
+ const statePath = path.join(dataDir, "state.db");
32
+ const obsPath = path.join(dataDir, "observability.db");
33
+ const stateDb = storageDb.createStateDatabase(statePath);
34
+ const observabilityDb = obsDb.createObservabilityDatabase(obsPath);
35
+ const deps = cliIndex.createCliRuntimeDeps({ stateDb, observabilityDb });
36
+ const runtimeResolved = boundary.resolvePackagedRuntime(PLUGIN_PACKAGE_ROOT);
37
+ const opsRouter = cliIndex.createOpsRouter({
38
+ runtimeAvailable: runtimeResolved.ok,
39
+ readModels: deps.readModels,
40
+ });
41
+ const commands = commandsMod.createCliCommands({
42
+ readModels: deps.readModels,
43
+ actionBridge: deps.actionBridge,
44
+ opsRouter,
45
+ });
46
+ const dispatch = async (command, input) => {
47
+ const def = commands.find((c) => c.name === command);
48
+ if (!def) {
49
+ return {
50
+ ok: false,
51
+ error: { code: "unknown_command", message: `Unknown Second Nature command: ${command}` },
52
+ };
53
+ }
54
+ const prevCwd = process.cwd();
55
+ try {
56
+ process.chdir(resolvedRoot);
57
+ return await def.execute(input);
58
+ }
59
+ finally {
60
+ process.chdir(prevCwd);
61
+ }
62
+ };
63
+ const close = () => {
64
+ cliIndex.closeCliRuntimeDeps(deps);
65
+ };
66
+ return { ok: true, workspaceRoot: resolvedRoot, dispatch, close };
67
+ }
68
+ catch (error) {
69
+ return {
70
+ ok: false,
71
+ error: {
72
+ code: "WORKSPACE_FULL_OPS_BRIDGE_FAILED",
73
+ message: error instanceof Error ? error.message : String(error),
74
+ nextStep: "Confirm the packaged plugin includes plugin/runtime and that the host allows dynamic import of sql.js. If the VM forbids it, use a subprocess workspace CLI (Plan B) or run outside the sandbox.",
75
+ },
76
+ };
77
+ }
78
+ }