@haaaiawd/second-nature 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -19,12 +19,102 @@
19
19
  * Test coverage:
20
20
  * - tests/integration/cli/plugin-runtime-registration.test.ts
21
21
  * - tests/integration/cli/plugin-packaging-walkthrough.test.ts
22
+ * - tests/integration/cli/plugin-workspace-ops-bridge.test.ts (T1.1.4)
22
23
  */
23
24
  import { startRuntimeService, } from "./runtime/core/second-nature/runtime/service-entry.js";
24
25
  import { getLifecycleState, recordRegistration, } from "./runtime/core/second-nature/runtime/lifecycle-service.js";
26
+ import { openWorkspaceOpsBridge } from "./workspace-ops-bridge.js";
25
27
  const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
26
28
  const HOST_SAFE_LIMITATION_MESSAGE = "Host-safe plugin package keeps synchronous register/load semantics, but mutating workspace runtime flows remain unavailable here.";
27
29
  let activationSpine = null;
30
+ /** T1.1.4 — lazily opened full read bridge; closed when workspace root / resolution changes. */
31
+ let workspaceOpsBridge = null;
32
+ function disposeWorkspaceOpsBridge() {
33
+ if (workspaceOpsBridge) {
34
+ workspaceOpsBridge.close();
35
+ workspaceOpsBridge = null;
36
+ }
37
+ }
38
+ const WORKSPACE_BRIDGE_COMMANDS = new Set([
39
+ "status",
40
+ "quiet",
41
+ "report",
42
+ "session",
43
+ "explain",
44
+ "heartbeat_check",
45
+ "fallback",
46
+ "storage_smoke",
47
+ ]);
48
+ function isWorkspaceBridgeCommand(command, input) {
49
+ if (command === "credential") {
50
+ const action = typeof input?.action === "string" ? input.action : "show";
51
+ return action !== "verify";
52
+ }
53
+ return WORKSPACE_BRIDGE_COMMANDS.has(command);
54
+ }
55
+ async function ensureWorkspaceOpsBridge(spine) {
56
+ const root = spine.workspaceRootContext.runtimeRoot;
57
+ if (workspaceOpsBridge?.root === root) {
58
+ return { ok: true, dispatch: workspaceOpsBridge.dispatch };
59
+ }
60
+ disposeWorkspaceOpsBridge();
61
+ const opened = await openWorkspaceOpsBridge(root);
62
+ if (!opened.ok) {
63
+ return opened;
64
+ }
65
+ workspaceOpsBridge = { root, close: opened.close, dispatch: opened.dispatch };
66
+ return { ok: true, dispatch: opened.dispatch };
67
+ }
68
+ async function routeSecondNatureCommand(spine, command, input) {
69
+ const wr = spine.workspaceRootContext;
70
+ const useBridge = wr.resolution !== "unknown" && isWorkspaceBridgeCommand(command, input);
71
+ if (useBridge) {
72
+ const bridge = await ensureWorkspaceOpsBridge(spine);
73
+ if (!bridge.ok) {
74
+ return {
75
+ ok: false,
76
+ surfaceMode: "host_safe_carrier",
77
+ workspaceReadModelsEvaluated: false,
78
+ message: HOST_SAFE_LIMITATION_MESSAGE,
79
+ error: bridge.error,
80
+ data: {
81
+ workspaceRootResolution: wr.resolution,
82
+ bridgeAttempted: true,
83
+ declaredRoot: wr.declaredRoot,
84
+ },
85
+ };
86
+ }
87
+ return (await bridge.dispatch(command, input));
88
+ }
89
+ const def = spine.router.resolve(command);
90
+ if (!def) {
91
+ return { ok: false, message: `Unknown Second Nature command: ${command}` };
92
+ }
93
+ return def.execute(input);
94
+ }
95
+ function resolveWorkspaceRoot(toolWorkspaceRoot) {
96
+ const env = process.env.SECOND_NATURE_WORKSPACE_ROOT?.trim();
97
+ if (env) {
98
+ return { resolution: "env", declaredRoot: env, runtimeRoot: env };
99
+ }
100
+ const tool = toolWorkspaceRoot?.trim();
101
+ if (tool) {
102
+ return { resolution: "tool_args", declaredRoot: tool, runtimeRoot: tool };
103
+ }
104
+ return { resolution: "unknown", declaredRoot: undefined, runtimeRoot: process.cwd() };
105
+ }
106
+ function syncWorkspaceRootFromTool(spine, toolWorkspaceRoot) {
107
+ const next = resolveWorkspaceRoot(toolWorkspaceRoot);
108
+ const prev = spine.workspaceRootContext;
109
+ const changed = next.runtimeRoot !== prev.runtimeRoot || next.resolution !== prev.resolution;
110
+ if (changed) {
111
+ disposeWorkspaceOpsBridge();
112
+ }
113
+ spine.workspaceRootContext = next;
114
+ if (changed) {
115
+ spine.runtimeHandle = startRuntimeService({ workspaceRoot: next.runtimeRoot });
116
+ }
117
+ }
28
118
  function trimRuntimeEvidence(spine) {
29
119
  if (spine.runtimeEvidence.length > 12) {
30
120
  spine.runtimeEvidence.splice(0, spine.runtimeEvidence.length - 12);
@@ -88,56 +178,73 @@ function parseExplainSubject(subjectRaw) {
88
178
  function buildStatusPayload(spine) {
89
179
  const runtimeEvidence = latestRuntimeEvidence(spine);
90
180
  const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
181
+ const wr = spine.workspaceRootContext;
182
+ const needsRootHint = wr.resolution === "unknown";
91
183
  return {
92
- ok: true,
184
+ ok: false,
185
+ surfaceMode: "host_safe_carrier",
186
+ workspaceReadModelsEvaluated: false,
187
+ message: HOST_SAFE_LIMITATION_MESSAGE,
188
+ error: {
189
+ code: "WORKSPACE_READ_SURFACE_UNAVAILABLE",
190
+ message: "Aggregated status requires workspace state; the host-safe plugin does not load persisted read models on this surface.",
191
+ requiredUserInput: needsRootHint ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
192
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
193
+ },
93
194
  data: {
94
- runtime: {
195
+ workspaceRootResolution: wr.resolution,
196
+ carrier: {
95
197
  host: "openclaw-plugin",
96
198
  serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
97
199
  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: [],
200
+ lastRuntimeTraceId: runtimeEvidence?.traceId,
113
201
  },
114
202
  },
115
203
  };
116
204
  }
117
- function buildQuietPayload(scope) {
205
+ function buildQuietPayload(spine, scope) {
206
+ const wr = spine.workspaceRootContext;
118
207
  return {
119
- ok: true,
208
+ ok: false,
209
+ surfaceMode: "host_safe_carrier",
210
+ workspaceReadModelsEvaluated: false,
211
+ message: HOST_SAFE_LIMITATION_MESSAGE,
212
+ error: {
213
+ code: "QUIET_READ_SURFACE_UNAVAILABLE",
214
+ message: "Quiet read surface requires workspace runtime; not evaluated in host-safe carrier mode.",
215
+ requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
216
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
217
+ },
120
218
  data: {
121
219
  scope,
122
- mode: "unknown",
123
- sourceCount: 0,
124
- reportCount: 0,
125
- recentJournalCount: 0,
220
+ evaluated: false,
221
+ unavailableReason: "host_safe_carrier_no_workspace_db",
222
+ workspaceRootResolution: wr.resolution,
126
223
  },
127
224
  };
128
225
  }
129
- function buildReportPayload(day) {
226
+ function buildReportPayload(spine, day) {
227
+ const wr = spine.workspaceRootContext;
130
228
  return {
131
- ok: true,
229
+ ok: false,
230
+ surfaceMode: "host_safe_carrier",
231
+ workspaceReadModelsEvaluated: false,
232
+ message: HOST_SAFE_LIMITATION_MESSAGE,
233
+ error: {
234
+ code: "REPORT_READ_SURFACE_UNAVAILABLE",
235
+ message: "Daily report artifacts require workspace runtime.",
236
+ requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
237
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
238
+ },
132
239
  data: {
240
+ evaluated: false,
241
+ unavailableReason: "host_safe_carrier_no_workspace_db",
133
242
  day: day && day.trim() ? day : new Date().toISOString().slice(0, 10),
134
- summary: "",
135
- highlights: [],
136
- sourceRefs: [],
243
+ workspaceRootResolution: wr.resolution,
137
244
  },
138
245
  };
139
246
  }
140
- function buildSessionPayload(sessionId) {
247
+ function buildSessionPayload(spine, sessionId) {
141
248
  if (!sessionId) {
142
249
  return {
143
250
  ok: false,
@@ -149,26 +256,44 @@ function buildSessionPayload(sessionId) {
149
256
  },
150
257
  };
151
258
  }
259
+ const wr = spine.workspaceRootContext;
152
260
  return {
153
- ok: true,
261
+ ok: false,
262
+ surfaceMode: "host_safe_carrier",
263
+ workspaceReadModelsEvaluated: false,
264
+ message: HOST_SAFE_LIMITATION_MESSAGE,
265
+ error: {
266
+ code: "SESSION_READ_SURFACE_UNAVAILABLE",
267
+ message: "Session analytics require workspace state database.",
268
+ requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
269
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
270
+ },
154
271
  data: {
155
272
  requestedSessionId: sessionId,
156
- traceId: sessionId,
157
- decisionCount: 0,
158
- attemptCount: 0,
159
- governanceCount: 0,
160
- keyFactors: [],
161
- evidenceRefs: [],
273
+ evaluated: false,
274
+ unavailableReason: "host_safe_carrier_no_workspace_db",
275
+ workspaceRootResolution: wr.resolution,
162
276
  },
163
277
  };
164
278
  }
165
- function buildCredentialPayload(platformId) {
279
+ function buildCredentialPayload(spine, platformId) {
280
+ const wr = spine.workspaceRootContext;
166
281
  return {
167
- ok: true,
282
+ ok: false,
283
+ surfaceMode: "host_safe_carrier",
284
+ workspaceReadModelsEvaluated: false,
285
+ message: HOST_SAFE_LIMITATION_MESSAGE,
286
+ error: {
287
+ code: "CREDENTIAL_READ_SURFACE_UNAVAILABLE",
288
+ message: "Credential inspection requires workspace runtime on this surface.",
289
+ requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
290
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
291
+ },
168
292
  data: {
169
- platformId: platformId && platformId.trim() ? platformId : "unknown",
170
- status: "missing",
171
- nextStep: "provide_credential_context",
293
+ platformId: platformId && platformId.trim() ? platformId : undefined,
294
+ evaluated: false,
295
+ unavailableReason: "host_safe_carrier_no_workspace_db",
296
+ workspaceRootResolution: wr.resolution,
172
297
  },
173
298
  };
174
299
  }
@@ -198,23 +323,22 @@ function buildExplainPayload(spine, subjectRaw) {
198
323
  }
199
324
  return createUnavailableActionError("EXPLAIN_SUBJECT_INVALID", "invalid explain subject", ["subject"], "reinvoke_explain_with_supported_subject");
200
325
  }
201
- const runtimeEvidence = latestRuntimeEvidence(spine);
326
+ const wr = spine.workspaceRootContext;
202
327
  return {
203
- ok: true,
328
+ ok: false,
329
+ surfaceMode: "host_safe_carrier",
330
+ workspaceReadModelsEvaluated: false,
331
+ message: HOST_SAFE_LIMITATION_MESSAGE,
332
+ error: {
333
+ code: "EXPLAIN_READ_SURFACE_UNAVAILABLE",
334
+ message: "Evidence-backed explain requires persisted workspace read models; host-safe carrier did not evaluate operator explain (CH-11-02).",
335
+ requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
336
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
337
+ },
204
338
  data: {
205
339
  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",
340
+ evaluated: false,
341
+ workspaceRootResolution: wr.resolution,
218
342
  },
219
343
  };
220
344
  }
@@ -256,16 +380,18 @@ function buildHeartbeatCheckPayload(spine, input) {
256
380
  const runtimeEvidence = latestRuntimeEvidence(spine);
257
381
  const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
258
382
  const timestamp = typeof input?.timestamp === "string" && input.timestamp.trim().length > 0 ? input.timestamp : updatedAt;
383
+ const wr = spine.workspaceRootContext;
259
384
  return {
260
385
  ok: true,
261
- status: "heartbeat_ok",
262
- heartbeat: "HEARTBEAT_OK",
386
+ status: "runtime_carrier_only",
387
+ livedExperienceLoopClaimed: false,
263
388
  scope: "rhythm",
264
389
  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.",
390
+ reasons: ["runtime_carrier_only", "host_safe_bridge_ack"],
391
+ nextAction: "continue_carrier_surface_only",
392
+ 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
393
  data: {
394
+ workspaceRootResolution: wr.resolution,
269
395
  runtime: {
270
396
  host: "openclaw-plugin",
271
397
  serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
@@ -316,7 +442,7 @@ function createHostSafeRouter(spine) {
316
442
  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
443
  }
318
444
  const platformId = typeof input?.platformId === "string" ? input.platformId : undefined;
319
- return buildCredentialPayload(platformId);
445
+ return buildCredentialPayload(spine, platformId);
320
446
  },
321
447
  },
322
448
  {
@@ -324,7 +450,7 @@ function createHostSafeRouter(spine) {
324
450
  description: "Inspect Quiet lifecycle state",
325
451
  execute: async (input) => {
326
452
  const scope = typeof input?.scope === "string" ? input.scope : undefined;
327
- return buildQuietPayload(scope);
453
+ return buildQuietPayload(spine, scope);
328
454
  },
329
455
  },
330
456
  {
@@ -332,7 +458,7 @@ function createHostSafeRouter(spine) {
332
458
  description: "Show daily report artifacts",
333
459
  execute: async (input) => {
334
460
  const day = typeof input?.day === "string" ? input.day : undefined;
335
- return buildReportPayload(day);
461
+ return buildReportPayload(spine, day);
336
462
  },
337
463
  },
338
464
  {
@@ -340,7 +466,7 @@ function createHostSafeRouter(spine) {
340
466
  description: "Inspect continuity session details",
341
467
  execute: async (input) => {
342
468
  const sessionId = typeof input?.sessionId === "string" ? input.sessionId : undefined;
343
- return buildSessionPayload(sessionId);
469
+ return buildSessionPayload(spine, sessionId);
344
470
  },
345
471
  },
346
472
  {
@@ -383,12 +509,14 @@ function createHostSafeRouter(spine) {
383
509
  };
384
510
  }
385
511
  function createActivationSpine() {
512
+ const workspaceRootContext = resolveWorkspaceRoot(undefined);
386
513
  const spine = {
387
514
  router: undefined,
388
- runtimeHandle: startRuntimeService({ workspaceRoot: process.cwd() }),
515
+ runtimeHandle: startRuntimeService({ workspaceRoot: workspaceRootContext.runtimeRoot }),
389
516
  lifecycleState: getLifecycleState(),
390
517
  serviceStartRecorded: false,
391
518
  runtimeEvidence: [],
519
+ workspaceRootContext,
392
520
  };
393
521
  spine.router = createHostSafeRouter(spine);
394
522
  return spine;
@@ -422,7 +550,14 @@ function recordRuntimeEvidence(spine, origin) {
422
550
  }
423
551
  function refreshRegistrationState() {
424
552
  const spine = ensureActivationSpine();
425
- spine.runtimeHandle = startRuntimeService({ workspaceRoot: process.cwd() });
553
+ const workspaceRootContext = resolveWorkspaceRoot(undefined);
554
+ const prev = spine.workspaceRootContext;
555
+ const changed = workspaceRootContext.runtimeRoot !== prev.runtimeRoot || workspaceRootContext.resolution !== prev.resolution;
556
+ if (changed) {
557
+ disposeWorkspaceOpsBridge();
558
+ }
559
+ spine.workspaceRootContext = workspaceRootContext;
560
+ spine.runtimeHandle = startRuntimeService({ workspaceRoot: workspaceRootContext.runtimeRoot });
426
561
  spine.lifecycleState = recordRegistration();
427
562
  spine.serviceStartRecorded = false;
428
563
  recordRuntimeEvidence(spine, "register");
@@ -575,7 +710,7 @@ export default {
575
710
  text: JSON.stringify({ ok: false, command: parsed.command, message: "Unknown Second Nature command." }),
576
711
  };
577
712
  }
578
- const result = await resolved.execute(parsed.input);
713
+ const result = await routeSecondNatureCommand(spine, parsed.command, parsed.input);
579
714
  return {
580
715
  text: JSON.stringify(result),
581
716
  };
@@ -590,11 +725,16 @@ export default {
590
725
  properties: {
591
726
  command: { type: "string" },
592
727
  args: { type: "object", additionalProperties: true },
728
+ workspaceRoot: {
729
+ type: "string",
730
+ description: "Workspace root for packaged smoke/runtime alignment (optional; prefer SECOND_NATURE_WORKSPACE_ROOT).",
731
+ },
593
732
  },
594
733
  required: ["command"],
595
734
  },
596
735
  async execute(_id, params) {
597
736
  const spine = ensureActivationSpine();
737
+ syncWorkspaceRootFromTool(spine, params.workspaceRoot);
598
738
  const resolved = spine.router.resolve(params.command);
599
739
  if (!resolved) {
600
740
  return {
@@ -606,7 +746,7 @@ export default {
606
746
  ],
607
747
  };
608
748
  }
609
- const result = await resolved.execute(params.args);
749
+ const result = await routeSecondNatureCommand(spine, params.command, params.args);
610
750
  return {
611
751
  content: [
612
752
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.1.9",
4
+ "version": "0.1.10",
5
5
  "entry": "./index.js",
6
6
  "description": "OpenClaw native plugin package with synchronous surface registration and a bundled runtime spine.",
7
7
  "capabilities": {
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.10",
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
  };
@@ -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
+ }