@cspeach/cli 0.9.0 → 1.0.0
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/README.md +1 -1
- package/dist/agent/intent-system-prompt.js +1 -1
- package/dist/agent/loop.js +209 -20
- package/dist/agent/providers/license-gate.js +44 -0
- package/dist/agent/skill-checkpoint.js +1 -1
- package/dist/agent/tool-dispatch.js +15 -0
- package/dist/approvals/canonical.js +91 -0
- package/dist/approvals/jwt.js +39 -2
- package/dist/auth/org-anthropic-key.js +25 -0
- package/dist/classifier/client.js +18 -3
- package/dist/commands/config-set.js +95 -0
- package/dist/commands/login.js +31 -14
- package/dist/commands/plan-model-tier.js +83 -0
- package/dist/commands/plan-resume.js +148 -21
- package/dist/config/loader.js +95 -1
- package/dist/doctor/checks/_http-probe.js +1 -0
- package/dist/doctor/checks/cert.js +14 -3
- package/dist/doctor/checks/sap.js +30 -8
- package/dist/doctor/checks/zcspeach.js +19 -4
- package/dist/one-shot.js +52 -4
- package/dist/projects/answer-blockers.js +137 -0
- package/dist/projects/extract-cca.js +108 -16
- package/dist/projects/extract-modernize.js +1 -1
- package/dist/projects/extract-plan.js +130 -37
- package/dist/projects/extract-spec-gap.js +34 -7
- package/dist/projects/extract-test-coverage.js +1 -1
- package/dist/projects/extract-upgrade.js +113 -22
- package/dist/projects/index.js +5 -2
- package/dist/projects/merge-cca.js +292 -0
- package/dist/projects/merge-upgrade.js +173 -0
- package/dist/projects/migration.js +103 -1
- package/dist/projects/output-paths.js +27 -0
- package/dist/projects/plan-run.js +159 -25
- package/dist/projects/plan-schema.js +63 -3
- package/dist/projects/promote-command.js +25 -2
- package/dist/projects/promote.js +128 -0
- package/dist/projects/save-command.js +247 -20
- package/dist/projects/status.js +3 -1
- package/dist/projects/validate.js +1 -1
- package/dist/projects/workspace.js +164 -20
- package/dist/renderer/notices.js +64 -0
- package/dist/renderer/progress-chatter.js +8 -0
- package/dist/renderer/tool-widget.js +18 -4
- package/dist/renderer/tty.js +43 -4
- package/dist/renderer/verify-chain.js +77 -0
- package/dist/repl/at-picker.js +60 -7
- package/dist/repl/builtin-commands.js +37 -0
- package/dist/repl/early-line-buffer.js +68 -0
- package/dist/repl/inquirer-guard.js +70 -5
- package/dist/repl/numbered-menu.js +131 -0
- package/dist/repl/post-turn-status.js +2 -2
- package/dist/repl/rule8-detector.js +17 -2
- package/dist/repl/safety-confirm.js +111 -2
- package/dist/repl/safety-mode-state.js +19 -3
- package/dist/repl/slash-picker.js +10 -15
- package/dist/repl.js +301 -35
- package/dist/router/classifier.js +150 -6
- package/dist/sap/capability-matrix.js +20 -0
- package/dist/sap/capability-matrix.json +11236 -0
- package/dist/sap/capability.js +146 -0
- package/dist/sap/connection-manager.js +19 -1
- package/dist/sap/onboarding.js +42 -4
- package/dist/session/pending.js +27 -0
- package/dist/skill-catalog.js +48 -43
- package/dist/skills/bundled-skills.js +279 -1
- package/dist/skills/promotion-dispatch.js +23 -0
- package/dist/tools/_command-shared.js +36 -12
- package/dist/tools/_filesystem-shared.js +139 -4
- package/dist/tools/_flag.js +25 -0
- package/dist/tools/approval.js +64 -21
- package/dist/tools/ask-question.js +96 -4
- package/dist/tools/capability/tool.js +74 -0
- package/dist/tools/dispatch-skill.js +22 -1
- package/dist/tools/extend-model/anchored-insert.js +810 -0
- package/dist/tools/extend-model/tool.js +188 -0
- package/dist/tools/filesystem/extract-document.js +57 -0
- package/dist/tools/filesystem/file-edit.js +12 -2
- package/dist/tools/filesystem/file-read.js +2 -2
- package/dist/tools/filesystem/file-write.js +11 -2
- package/dist/tools/filesystem/glob.js +11 -0
- package/dist/tools/filesystem/grep.js +10 -0
- package/dist/tools/filesystem/read-document.js +107 -0
- package/dist/tools/fiori/apply.js +50 -0
- package/dist/tools/fiori/bin.js +3 -0
- package/dist/tools/fiori/catalog/index.js +27 -0
- package/dist/tools/fiori/catalog/value-help.js +230 -0
- package/dist/tools/fiori/catalog/viz-chart.js +177 -0
- package/dist/tools/fiori/cli.js +71 -0
- package/dist/tools/fiori/deploy-config.js +73 -0
- package/dist/tools/fiori/fe-scaffold.js +45 -0
- package/dist/tools/fiori/i18n.js +39 -0
- package/dist/tools/fiori/manifest.js +70 -0
- package/dist/tools/fiori/render.js +77 -0
- package/dist/tools/fiori/scaffold.js +39 -0
- package/dist/tools/fiori/tools.js +356 -0
- package/dist/tools/fiori/types.js +1 -0
- package/dist/tools/local-build.js +76 -0
- package/dist/tools/local-files.js +31 -0
- package/dist/tools/project/_merge-shared.js +68 -0
- package/dist/tools/project/cca_merge.js +164 -0
- package/dist/tools/project/playbook_get.js +1 -1
- package/dist/tools/project/upgrade_merge_progress.js +206 -0
- package/dist/tools/sap-read.js +53 -9
- package/dist/tools/sap-write.js +530 -21
- package/dist/tools/shell/shell_exec.js +41 -6
- package/dist/tools/snapshot.js +37 -14
- package/dist/tools/subagent/background_run.js +17 -1
- package/dist/tools/transport-resolution.js +86 -0
- package/dist/tools/transport.js +224 -5
- package/dist/tools/write-mode.js +4 -0
- package/dist/ui/app.js +6 -2
- package/dist/ui/body.js +13 -0
- package/dist/ui/footer.js +20 -6
- package/dist/ui/line-resolution.js +17 -6
- package/dist/ui/session-timeline.js +1 -0
- package/dist/ui/text-input.js +150 -0
- package/dist/ui/widgets/ask-question-modal.js +4 -1
- package/package.json +19 -3
- package/bench/README.md +0 -78
- package/bench/prompts/abap-document-cds.md +0 -44
- package/bench/prompts/abap-explain-bdef-handler.md +0 -57
- package/bench/prompts/abap-test-method.md +0 -42
- package/bench/results/abap-document-cds/claude-haiku-4-5.md +0 -189
- package/bench/results/abap-document-cds/claude-opus-4-7.md +0 -120
- package/bench/results/abap-document-cds/claude-sonnet-4-6.md +0 -151
- package/bench/results/abap-explain-bdef-handler/claude-haiku-4-5.md +0 -112
- package/bench/results/abap-explain-bdef-handler/claude-opus-4-7.md +0 -101
- package/bench/results/abap-explain-bdef-handler/claude-sonnet-4-6.md +0 -101
- package/bench/results/abap-test-method/claude-haiku-4-5.md +0 -186
- package/bench/results/abap-test-method/claude-opus-4-7.md +0 -193
- package/bench/results/abap-test-method/claude-sonnet-4-6.md +0 -234
package/dist/tools/sap-write.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* lock(type, name) → Promise<LockResult> line 1914
|
|
16
16
|
* unlock(type, name, lockHandle) → Promise<void> line 1949
|
|
17
17
|
* serviceBindingPublish(name) → Promise<{status,error?}> line 2658
|
|
18
|
+
* serviceBindingUnpublish(name) → Promise<{status,error?}>
|
|
18
19
|
* messageMaintain (STUB — not yet in sap-client)
|
|
19
20
|
* numberRangeIntervals (STUB — not yet in sap-client)
|
|
20
21
|
*
|
|
@@ -24,18 +25,53 @@ import crypto from 'node:crypto';
|
|
|
24
25
|
import { registerTool } from './index.js';
|
|
25
26
|
import { verifyAndSpendApprovalId } from '../approvals/jwt.js';
|
|
26
27
|
import { autoSnapshot } from './snapshot.js';
|
|
28
|
+
import { buildWriteChainLine } from '../renderer/verify-chain.js';
|
|
27
29
|
import { verifySyntax, verifyActive } from './verify.js';
|
|
28
30
|
import { appendPending, finalizeToolCall } from '../session/pending.js';
|
|
29
31
|
import { auditLog } from '@cspeach/sap-client';
|
|
30
32
|
import { ERR } from '../errors/codes.js';
|
|
31
33
|
import { interceptUpdateMethod, CancelledByUserError, } from './update-method-intercept.js';
|
|
34
|
+
import { resolveWriteTransport } from './transport-resolution.js';
|
|
32
35
|
import { presentSafetyConfirmation } from '../repl/safety-confirm.js';
|
|
33
36
|
import { isSafetyConfirmEnabled } from '../repl/safety-mode-state.js';
|
|
37
|
+
// ── Transcript visibility helpers (D16-display + tour slide-7, 2026-06-12) ──
|
|
38
|
+
//
|
|
39
|
+
// Rule 7 auto-snapshots always fired but the transcript never said so
|
|
40
|
+
// (battery D16-display). And the tour shows writes as
|
|
41
|
+
// "① snapshot ✓ ② written ✓ ③ syntax ✓ ④ activated ✓" while real
|
|
42
|
+
// transcripts showed an opaque tool row. Both lines below are HUMAN-stream
|
|
43
|
+
// only (ctx.chunkEmitter) — tool RESULT payloads the model sees are
|
|
44
|
+
// unchanged, and subagent ctxs (no chunkEmitter) stay silent.
|
|
45
|
+
/** Dim one-liner when the Rule 7 auto-snapshot actually captured source. */
|
|
46
|
+
function emitSnapshotNotice(ctx, snap, type, name) {
|
|
47
|
+
if (snap?.taken && snap.entry) {
|
|
48
|
+
ctx.chunkEmitter?.emit('info', `snapshot ✓ ${type} ${name} ${snap.entry.timestamp}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Map the snapshot-gate outcome onto the chain line's honest states.
|
|
52
|
+
* `undefined` covers the best-effort gates whose failure was swallowed.
|
|
53
|
+
* 'object_not_found' is a genuine ADT 404 only; any other read failure maps
|
|
54
|
+
* to 'source_read_failed' (+ short detail) — never to "(new object)". */
|
|
55
|
+
function snapshotChainState(snap) {
|
|
56
|
+
if (snap?.taken)
|
|
57
|
+
return { snapshot: 'taken' };
|
|
58
|
+
if (snap?.reason === 'object_not_found')
|
|
59
|
+
return { snapshot: 'new_object' };
|
|
60
|
+
if (snap?.reason === 'source_read_failed') {
|
|
61
|
+
return { snapshot: 'source_read_failed', snapshotDetail: snap.detail };
|
|
62
|
+
}
|
|
63
|
+
return { snapshot: 'best_effort_failed' };
|
|
64
|
+
}
|
|
65
|
+
/** Compact ①②③④ pipeline summary — derived from the real gate results. */
|
|
66
|
+
function emitWriteChain(ctx, input) {
|
|
67
|
+
ctx.chunkEmitter?.emit('chunk', buildWriteChainLine(input) + '\n');
|
|
68
|
+
}
|
|
34
69
|
registerTool({
|
|
35
70
|
name: 'sap_set_source',
|
|
36
71
|
description: 'Overwrite the full source of an ABAP object. Requires approval_id. '
|
|
37
72
|
+ 'Use for class-definition changes, new methods, or any change not confined to '
|
|
38
|
-
+ 'a single method body. For method-body-only edits use sap_update_method (Rule 7a).'
|
|
73
|
+
+ 'a single method body. For method-body-only edits use sap_update_method (Rule 7a). '
|
|
74
|
+
+ 'The transport auto-resolves to the owning request if the object is already locked.',
|
|
39
75
|
isMutating: true,
|
|
40
76
|
input_schema: {
|
|
41
77
|
type: 'object',
|
|
@@ -57,7 +93,7 @@ registerTool({
|
|
|
57
93
|
is_error: true,
|
|
58
94
|
};
|
|
59
95
|
}
|
|
60
|
-
const toolUseId = crypto.randomUUID();
|
|
96
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
61
97
|
const startedAt = Date.now();
|
|
62
98
|
// ── Rule 9 conflict warning ──────────────────────────────────────────────
|
|
63
99
|
// Emit a warning if the LLM passed an explicit transport that differs from
|
|
@@ -69,9 +105,16 @@ registerTool({
|
|
|
69
105
|
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
70
106
|
}
|
|
71
107
|
}
|
|
72
|
-
// ──
|
|
108
|
+
// ── Rule 9 owning-transport check (D15/D20) ──────────────────────────────
|
|
109
|
+
// If the object is already locked in an open request, write under THAT
|
|
110
|
+
// request — any other transport is a guaranteed CTS lock conflict.
|
|
111
|
+
const resolved = await resolveWriteTransport(ctx, args.type, args.name, args.transport);
|
|
112
|
+
// ── Gate 2: Snapshot (Rule 7) — HARD gate ────────────────────────────────
|
|
113
|
+
// Only a genuine 404 (new object) may proceed snapshot-less. A read
|
|
114
|
+
// failure on a possibly-EXISTING object means no recovery copy → block.
|
|
115
|
+
let snapOutcome;
|
|
73
116
|
try {
|
|
74
|
-
await autoSnapshot(args.name, args.type, ctx.adt);
|
|
117
|
+
snapOutcome = await autoSnapshot(args.name, args.type, ctx.adt);
|
|
75
118
|
}
|
|
76
119
|
catch (err) {
|
|
77
120
|
return {
|
|
@@ -79,6 +122,18 @@ registerTool({
|
|
|
79
122
|
is_error: true,
|
|
80
123
|
};
|
|
81
124
|
}
|
|
125
|
+
if (snapOutcome?.reason === 'source_read_failed') {
|
|
126
|
+
return {
|
|
127
|
+
content: JSON.stringify({
|
|
128
|
+
error: ERR.SNAPSHOT_FAILED,
|
|
129
|
+
reason: 'source_read_failed',
|
|
130
|
+
detail: snapOutcome.detail,
|
|
131
|
+
recovery: 'Pre-write source read failed (non-404) — the object may exist but no snapshot could be taken. Fix the read failure (connection/auth) and retry; do not bypass Rule 7.',
|
|
132
|
+
}),
|
|
133
|
+
is_error: true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
emitSnapshotNotice(ctx, snapOutcome, args.type, args.name);
|
|
82
137
|
// ── Gate 3: Safety confirmation (Rule 7) ─────────────────────────────────
|
|
83
138
|
if (isSafetyConfirmEnabled()) {
|
|
84
139
|
const r = await presentSafetyConfirmation({
|
|
@@ -87,7 +142,7 @@ registerTool({
|
|
|
87
142
|
object: { type: args.type, name: args.name },
|
|
88
143
|
what_will_happen: 'Overwrites full object source; snapshot is taken first.',
|
|
89
144
|
rollback_available: true,
|
|
90
|
-
transport:
|
|
145
|
+
transport: resolved.transport ?? ctx.currentTransport?.get() ?? '',
|
|
91
146
|
});
|
|
92
147
|
if (!r.confirmed) {
|
|
93
148
|
return {
|
|
@@ -114,7 +169,7 @@ registerTool({
|
|
|
114
169
|
// Regression evidence + diagnosis: memory/project_phase3_lite_regression.md.
|
|
115
170
|
let writeResult;
|
|
116
171
|
try {
|
|
117
|
-
writeResult = await ctx.adt.setSource(args.type, args.name, args.source,
|
|
172
|
+
writeResult = await ctx.adt.setSource(args.type, args.name, args.source, resolved.transport);
|
|
118
173
|
}
|
|
119
174
|
catch (err) {
|
|
120
175
|
const errStr = String(err);
|
|
@@ -140,12 +195,19 @@ registerTool({
|
|
|
140
195
|
// ── Gate 4: Verify — syntax check (Rule 10) ──────────────────────────────
|
|
141
196
|
const syntax = await verifySyntax(ctx.adt, args.name, args.type);
|
|
142
197
|
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ written: true, syntax }), false, Date.now() - startedAt);
|
|
198
|
+
// Tour slide-7 chain — derived from the gates that actually ran.
|
|
199
|
+
emitWriteChain(ctx, {
|
|
200
|
+
...snapshotChainState(snapOutcome),
|
|
201
|
+
written: true,
|
|
202
|
+
syntax: { ok: syntax.ok, errors: syntax.errors },
|
|
203
|
+
});
|
|
143
204
|
return {
|
|
144
205
|
content: JSON.stringify({
|
|
145
206
|
written: true,
|
|
146
207
|
syntax_ok: syntax.ok,
|
|
147
208
|
errors: syntax.errors,
|
|
148
209
|
warnings: syntax.warnings,
|
|
210
|
+
...(resolved.owning ? { transport_used: resolved.owning, transport_note: resolved.note } : {}),
|
|
149
211
|
next_step: syntax.ok
|
|
150
212
|
? 'activation_allowed'
|
|
151
213
|
: 'activation_blocked_until_syntax_errors_fixed',
|
|
@@ -166,7 +228,8 @@ registerTool({
|
|
|
166
228
|
description: 'Replace the body of a single method in an existing class. '
|
|
167
229
|
+ 'Preferred over sap_set_source for method-body-only changes (Rule 7a) — '
|
|
168
230
|
+ 'does NOT rewrite the class definition. '
|
|
169
|
-
+ 'method_source must be the lines between METHOD x. and ENDMETHOD. (no wrappers).'
|
|
231
|
+
+ 'method_source must be the lines between METHOD x. and ENDMETHOD. (no wrappers). '
|
|
232
|
+
+ 'The transport auto-resolves to the owning request if the class is already locked.',
|
|
170
233
|
isMutating: true,
|
|
171
234
|
input_schema: {
|
|
172
235
|
type: 'object',
|
|
@@ -188,7 +251,7 @@ registerTool({
|
|
|
188
251
|
is_error: true,
|
|
189
252
|
};
|
|
190
253
|
}
|
|
191
|
-
const toolUseId = crypto.randomUUID();
|
|
254
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
192
255
|
const startedAt = Date.now();
|
|
193
256
|
// ── Rule 9 conflict warning ──────────────────────────────────────────────
|
|
194
257
|
// Emit a warning if the LLM passed an explicit transport that differs from
|
|
@@ -200,9 +263,15 @@ registerTool({
|
|
|
200
263
|
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
201
264
|
}
|
|
202
265
|
}
|
|
203
|
-
// ──
|
|
266
|
+
// ── Rule 9 owning-transport check (D15/D20) ──────────────────────────────
|
|
267
|
+
const resolved = await resolveWriteTransport(ctx, 'CLAS', args.class_name, args.transport);
|
|
268
|
+
// ── Gate 2: Snapshot (Rule 7) — HARD gate ────────────────────────────────
|
|
269
|
+
// sap_update_method always targets an EXISTING class, so a read failure
|
|
270
|
+
// (non-404) means no recovery copy → block. (A genuine 404 falls through
|
|
271
|
+
// here but the preview gate below fails on the same missing class.)
|
|
272
|
+
let snapOutcome;
|
|
204
273
|
try {
|
|
205
|
-
await autoSnapshot(args.class_name, 'CLAS', ctx.adt);
|
|
274
|
+
snapOutcome = await autoSnapshot(args.class_name, 'CLAS', ctx.adt);
|
|
206
275
|
}
|
|
207
276
|
catch (err) {
|
|
208
277
|
return {
|
|
@@ -210,6 +279,18 @@ registerTool({
|
|
|
210
279
|
is_error: true,
|
|
211
280
|
};
|
|
212
281
|
}
|
|
282
|
+
if (snapOutcome?.reason === 'source_read_failed') {
|
|
283
|
+
return {
|
|
284
|
+
content: JSON.stringify({
|
|
285
|
+
error: ERR.SNAPSHOT_FAILED,
|
|
286
|
+
reason: 'source_read_failed',
|
|
287
|
+
detail: snapOutcome.detail,
|
|
288
|
+
recovery: 'Pre-write source read failed (non-404) — the class exists but no snapshot could be taken. Fix the read failure (connection/auth) and retry; do not bypass Rule 7.',
|
|
289
|
+
}),
|
|
290
|
+
is_error: true,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
emitSnapshotNotice(ctx, snapOutcome, 'CLAS', args.class_name);
|
|
213
294
|
// ── Gate 3: Diff preview & confirmation (Rule 7a) ────────────────────────
|
|
214
295
|
if (!ctx.previewHook) {
|
|
215
296
|
return {
|
|
@@ -227,7 +308,7 @@ registerTool({
|
|
|
227
308
|
className: args.class_name,
|
|
228
309
|
methodName: args.method_name,
|
|
229
310
|
methodSource: args.method_source,
|
|
230
|
-
transport:
|
|
311
|
+
transport: resolved.transport ?? ctx.currentTransport?.get() ?? '',
|
|
231
312
|
};
|
|
232
313
|
try {
|
|
233
314
|
await interceptUpdateMethod(minimal, call, ctx.previewHook);
|
|
@@ -263,7 +344,7 @@ registerTool({
|
|
|
263
344
|
// silently overwriting the inactive-only declarations.
|
|
264
345
|
let updateResult;
|
|
265
346
|
try {
|
|
266
|
-
updateResult = await ctx.adt.updateMethod(args.class_name, args.method_name, args.method_source,
|
|
347
|
+
updateResult = await ctx.adt.updateMethod(args.class_name, args.method_name, args.method_source, resolved.transport);
|
|
267
348
|
}
|
|
268
349
|
catch (err) {
|
|
269
350
|
const errStr = String(err);
|
|
@@ -302,12 +383,19 @@ registerTool({
|
|
|
302
383
|
// ── Gate 4: Verify — syntax check (Rule 10) ──────────────────────────────
|
|
303
384
|
const syntax = await verifySyntax(ctx.adt, args.class_name, 'CLAS');
|
|
304
385
|
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ updated: true, syntax }), false, Date.now() - startedAt);
|
|
386
|
+
// Tour slide-7 chain — derived from the gates that actually ran.
|
|
387
|
+
emitWriteChain(ctx, {
|
|
388
|
+
...snapshotChainState(snapOutcome),
|
|
389
|
+
written: true,
|
|
390
|
+
syntax: { ok: syntax.ok, errors: syntax.errors },
|
|
391
|
+
});
|
|
305
392
|
return {
|
|
306
393
|
content: JSON.stringify({
|
|
307
394
|
updated: true,
|
|
308
395
|
syntax_ok: syntax.ok,
|
|
309
396
|
errors: syntax.errors,
|
|
310
397
|
warnings: syntax.warnings,
|
|
398
|
+
...(resolved.owning ? { transport_used: resolved.owning, transport_note: resolved.note } : {}),
|
|
311
399
|
next_step: syntax.ok
|
|
312
400
|
? 'activation_allowed'
|
|
313
401
|
: 'activation_blocked_until_syntax_errors_fixed',
|
|
@@ -324,7 +412,8 @@ registerTool({
|
|
|
324
412
|
name: 'sap_delete_object',
|
|
325
413
|
description: 'Delete an ABAP object. Requires approval_id. '
|
|
326
414
|
+ 'A snapshot is taken automatically before deletion. '
|
|
327
|
-
+ 'Post-check: confirms the object no longer exists.'
|
|
415
|
+
+ 'Post-check: confirms the object no longer exists. '
|
|
416
|
+
+ 'The transport auto-resolves to the owning request if the object is already locked.',
|
|
328
417
|
isMutating: true,
|
|
329
418
|
input_schema: {
|
|
330
419
|
type: 'object',
|
|
@@ -345,7 +434,7 @@ registerTool({
|
|
|
345
434
|
is_error: true,
|
|
346
435
|
};
|
|
347
436
|
}
|
|
348
|
-
const toolUseId = crypto.randomUUID();
|
|
437
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
349
438
|
const startedAt = Date.now();
|
|
350
439
|
// ── Rule 9 conflict warning ──────────────────────────────────────────────
|
|
351
440
|
// Emit a warning if the LLM passed an explicit transport that differs from
|
|
@@ -357,13 +446,17 @@ registerTool({
|
|
|
357
446
|
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
358
447
|
}
|
|
359
448
|
}
|
|
449
|
+
// ── Rule 9 owning-transport check (D15/D20) ──────────────────────────────
|
|
450
|
+
const resolved = await resolveWriteTransport(ctx, args.type, args.name, args.transport);
|
|
360
451
|
// ── Gate 2: Snapshot (Rule 7) — best-effort, object may not have source ──
|
|
452
|
+
let snapOutcome;
|
|
361
453
|
try {
|
|
362
|
-
await autoSnapshot(args.name, args.type, ctx.adt);
|
|
454
|
+
snapOutcome = await autoSnapshot(args.name, args.type, ctx.adt);
|
|
363
455
|
}
|
|
364
456
|
catch {
|
|
365
457
|
// Silently ignore — object may be a shell with no readable source.
|
|
366
458
|
}
|
|
459
|
+
emitSnapshotNotice(ctx, snapOutcome, args.type, args.name);
|
|
367
460
|
// ── Gate 3: Safety confirmation (Rule 7 — delete is irreversible-ish) ────
|
|
368
461
|
if (isSafetyConfirmEnabled()) {
|
|
369
462
|
const r = await presentSafetyConfirmation({
|
|
@@ -372,7 +465,7 @@ registerTool({
|
|
|
372
465
|
object: { type: args.type, name: args.name },
|
|
373
466
|
what_will_happen: 'Deletes object from SAP; snapshot is taken first; recovery via sap_snapshot_read + sap_set_source (LLM composes the rollback).',
|
|
374
467
|
rollback_available: true,
|
|
375
|
-
transport:
|
|
468
|
+
transport: resolved.transport ?? ctx.currentTransport?.get() ?? '',
|
|
376
469
|
});
|
|
377
470
|
if (!r.confirmed) {
|
|
378
471
|
return {
|
|
@@ -394,7 +487,7 @@ registerTool({
|
|
|
394
487
|
// 4xx. Must check status / error on the return value.
|
|
395
488
|
let deleteResult;
|
|
396
489
|
try {
|
|
397
|
-
deleteResult = await ctx.adt.deleteObject(args.type, args.name,
|
|
490
|
+
deleteResult = await ctx.adt.deleteObject(args.type, args.name, resolved.transport);
|
|
398
491
|
}
|
|
399
492
|
catch (err) {
|
|
400
493
|
const errStr = String(err);
|
|
@@ -423,7 +516,11 @@ registerTool({
|
|
|
423
516
|
.catch(() => false);
|
|
424
517
|
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ deleted: !stillExists }), false, Date.now() - startedAt);
|
|
425
518
|
return {
|
|
426
|
-
content: JSON.stringify({
|
|
519
|
+
content: JSON.stringify({
|
|
520
|
+
deleted: !stillExists,
|
|
521
|
+
still_exists: stillExists,
|
|
522
|
+
...(resolved.owning ? { transport_used: resolved.owning, transport_note: resolved.note } : {}),
|
|
523
|
+
}),
|
|
427
524
|
};
|
|
428
525
|
},
|
|
429
526
|
});
|
|
@@ -448,6 +545,11 @@ registerTool({
|
|
|
448
545
|
description: { type: 'string', description: 'Short description' },
|
|
449
546
|
transport: { type: 'string', description: 'Target transport (optional — required for workbench packages)' },
|
|
450
547
|
approval_id: { type: 'string', description: 'JWT from request_approval' },
|
|
548
|
+
// SRVB-only: a service binding must name the activated service definition it
|
|
549
|
+
// binds, plus the protocol/version. Required for type=SRVB; ignored otherwise.
|
|
550
|
+
serviceDefinition: { type: 'string', description: 'SRVB ONLY — REQUIRED when type=SRVB: name of the activated service definition (SRVD) to bind, e.g. ZUI_MYSERVICE.' },
|
|
551
|
+
bindingType: { type: 'string', description: 'SRVB ONLY (optional): binding protocol. Default ODATA.' },
|
|
552
|
+
bindingVersion: { type: 'string', description: 'SRVB ONLY (optional): binding version. Default V4 (OData V4 UI). Use V2 for OData V2.' },
|
|
451
553
|
},
|
|
452
554
|
required: ['name', 'type', 'package', 'description', 'approval_id'],
|
|
453
555
|
},
|
|
@@ -460,7 +562,7 @@ registerTool({
|
|
|
460
562
|
is_error: true,
|
|
461
563
|
};
|
|
462
564
|
}
|
|
463
|
-
const toolUseId = crypto.randomUUID();
|
|
565
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
464
566
|
const startedAt = Date.now();
|
|
465
567
|
// No snapshot — new object has no prior source.
|
|
466
568
|
// ── Rule 9 conflict warning + 3-tier transport fallback ──────────────────
|
|
@@ -486,7 +588,11 @@ registerTool({
|
|
|
486
588
|
// createObject(objtype, name, packageName, description, transport?, extras?)
|
|
487
589
|
let result;
|
|
488
590
|
try {
|
|
489
|
-
result = await ctx.adt.createObject(args.type, args.name, args.package, args.description, effectiveTransport
|
|
591
|
+
result = await ctx.adt.createObject(args.type, args.name, args.package, args.description, effectiveTransport, {
|
|
592
|
+
serviceDefinition: args.serviceDefinition,
|
|
593
|
+
bindingType: args.bindingType,
|
|
594
|
+
bindingVersion: args.bindingVersion,
|
|
595
|
+
});
|
|
490
596
|
await auditLog('createObject', args.type, args.name, 'success', Date.now() - startedAt);
|
|
491
597
|
}
|
|
492
598
|
catch (err) {
|
|
@@ -516,6 +622,386 @@ registerTool({
|
|
|
516
622
|
};
|
|
517
623
|
},
|
|
518
624
|
});
|
|
625
|
+
// ── sap_set_class_include ─────────────────────────────────────────────────────
|
|
626
|
+
//
|
|
627
|
+
// Signature: setClassInclude(className, includeType, source, transport?)
|
|
628
|
+
// → Promise<ClassIncludeResult>
|
|
629
|
+
// Writes a class local-include source (CCDEF/CCIMP/CCMAC/test classes). This is
|
|
630
|
+
// how RAP behaviour-pool handlers (which live in CCIMP / locals_imp) get landed.
|
|
631
|
+
// AdtClient locks the CLASS object, tries a ladder of PUT URL/content-type
|
|
632
|
+
// variants (first 2xx wins), and always unlocks. Snapshot is best-effort:
|
|
633
|
+
// the include source may not be independently readable, so a failed snapshot
|
|
634
|
+
// must not block the write (consistent with sap_delete_object).
|
|
635
|
+
registerTool({
|
|
636
|
+
name: 'sap_set_class_include',
|
|
637
|
+
description: 'Write a class local-include source (CCDEF locals_def, CCIMP locals_imp, '
|
|
638
|
+
+ 'CCMAC macros, or test classes). Required for RAP behaviour-pool handlers, '
|
|
639
|
+
+ 'which live in the CCIMP (locals_imp) include. Requires approval_id. '
|
|
640
|
+
+ 'After writing, activate the class via sap_activate. '
|
|
641
|
+
+ 'The transport auto-resolves to the owning request if the class is already locked.',
|
|
642
|
+
isMutating: true,
|
|
643
|
+
input_schema: {
|
|
644
|
+
type: 'object',
|
|
645
|
+
properties: {
|
|
646
|
+
className: { type: 'string', description: 'ABAP class name (e.g. ZBP_FOO)' },
|
|
647
|
+
includeType: {
|
|
648
|
+
type: 'string',
|
|
649
|
+
enum: ['locals_def', 'locals_imp', 'macros', 'testclasses'],
|
|
650
|
+
description: 'Which local include to write (locals_imp = CCIMP, where RAP handlers live)',
|
|
651
|
+
},
|
|
652
|
+
source: { type: 'string', description: 'Include source body' },
|
|
653
|
+
transport: { type: 'string', description: 'Transport request number (optional)' },
|
|
654
|
+
approval_id: { type: 'string', description: 'JWT from request_approval' },
|
|
655
|
+
},
|
|
656
|
+
required: ['className', 'includeType', 'source', 'approval_id'],
|
|
657
|
+
},
|
|
658
|
+
handler: async (args, ctx) => {
|
|
659
|
+
// ── Gate 1: Approval ────────────────────────────────────────────────────
|
|
660
|
+
const verify = await verifyAndSpendApprovalId(args.approval_id, args.className, 'modify');
|
|
661
|
+
if (!verify.ok) {
|
|
662
|
+
return {
|
|
663
|
+
content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
|
|
664
|
+
is_error: true,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
668
|
+
const startedAt = Date.now();
|
|
669
|
+
// ── Rule 9 conflict warning + 3-tier transport fallback ──────────────────
|
|
670
|
+
{
|
|
671
|
+
const sessionTransport = ctx.currentTransport?.get() ?? null;
|
|
672
|
+
if (args.transport && sessionTransport && args.transport !== sessionTransport) {
|
|
673
|
+
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const effectiveTransport = args.transport ?? ctx.currentTransport?.get() ?? undefined;
|
|
677
|
+
// ── Rule 9 owning-transport check (D15/D20) ──────────────────────────────
|
|
678
|
+
// The CCIMP piece (LIMU CINC) is exactly the kind of object that sits
|
|
679
|
+
// locked in a request from earlier work — battery S1's 409 root cause.
|
|
680
|
+
const resolved = await resolveWriteTransport(ctx, 'CLAS', args.className, effectiveTransport);
|
|
681
|
+
// ── Gate 2: Snapshot (Rule 7) — best-effort, include may not be readable ─
|
|
682
|
+
let snapOutcome;
|
|
683
|
+
try {
|
|
684
|
+
snapOutcome = await autoSnapshot(args.className, 'CLAS', ctx.adt);
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
// Silently ignore — include source may not be independently readable.
|
|
688
|
+
}
|
|
689
|
+
emitSnapshotNotice(ctx, snapOutcome, 'CLAS', args.className);
|
|
690
|
+
// ── WAL: record pending entry ────────────────────────────────────────────
|
|
691
|
+
await appendPending(ctx.session, {
|
|
692
|
+
tool_use_id: toolUseId,
|
|
693
|
+
tool: 'sap_set_class_include',
|
|
694
|
+
args: { className: args.className, includeType: args.includeType },
|
|
695
|
+
started_at: new Date().toISOString(),
|
|
696
|
+
});
|
|
697
|
+
// ── Write ────────────────────────────────────────────────────────────────
|
|
698
|
+
// setClassInclude THROWS only when it cannot acquire the class lock;
|
|
699
|
+
// for the all-variants-failed case it RETURNS { written:false, ... }.
|
|
700
|
+
// Both paths must surface as an error envelope.
|
|
701
|
+
let result;
|
|
702
|
+
try {
|
|
703
|
+
result = await ctx.adt.setClassInclude(args.className, args.includeType, args.source, resolved.transport);
|
|
704
|
+
}
|
|
705
|
+
catch (err) {
|
|
706
|
+
const errStr = String(err);
|
|
707
|
+
await finalizeToolCall(ctx.session, toolUseId, errStr, true, Date.now() - startedAt);
|
|
708
|
+
return {
|
|
709
|
+
content: JSON.stringify({ error: 'class_include_write_failed', detail: errStr }),
|
|
710
|
+
is_error: true,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
if (!result.written) {
|
|
714
|
+
// D14: `result.error` now carries SAP's T100 diagnosis verbatim when a
|
|
715
|
+
// variant hit a CTS/lock conflict — pass it through unaltered, plus the
|
|
716
|
+
// structured owning transport so the model can retry correctly instead
|
|
717
|
+
// of declaring the change manual-only.
|
|
718
|
+
const detail = result.error ?? `All PUT variants failed (last HTTP ${result.httpStatus})`;
|
|
719
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
|
|
720
|
+
return {
|
|
721
|
+
content: JSON.stringify({
|
|
722
|
+
error: 'class_include_write_failed',
|
|
723
|
+
detail,
|
|
724
|
+
http_status: result.httpStatus,
|
|
725
|
+
...(result.lockedInTransport ? {
|
|
726
|
+
locked_in_transport: result.lockedInTransport,
|
|
727
|
+
recovery: `Retry this exact write with transport=${result.lockedInTransport} — that request already owns the object.`,
|
|
728
|
+
} : {}),
|
|
729
|
+
attempts: result.attempts,
|
|
730
|
+
}),
|
|
731
|
+
is_error: true,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
await auditLog('setClassInclude', 'CLAS', args.className, 'success', Date.now() - startedAt, `include=${args.includeType}`);
|
|
735
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ written: true, variant: result.variantUsed }), false, Date.now() - startedAt);
|
|
736
|
+
// Tour slide-7 chain. Honest states: includes have no standalone syntax
|
|
737
|
+
// gate (checked at activation), so ③ renders as deferred, never faked.
|
|
738
|
+
emitWriteChain(ctx, {
|
|
739
|
+
...snapshotChainState(snapOutcome),
|
|
740
|
+
written: true,
|
|
741
|
+
syntax: 'not_checked',
|
|
742
|
+
});
|
|
743
|
+
return {
|
|
744
|
+
content: JSON.stringify({
|
|
745
|
+
written: true,
|
|
746
|
+
variant: result.variantUsed,
|
|
747
|
+
...(resolved.owning ? { transport_used: resolved.owning, transport_note: resolved.note } : {}),
|
|
748
|
+
next_step: 'activate_the_class',
|
|
749
|
+
}),
|
|
750
|
+
};
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
// ── sap_create_domain ────────────────────────────────────────────────────────
|
|
754
|
+
//
|
|
755
|
+
// Signature: createDomain(spec, transport?) → Promise<DdicCreateResult>
|
|
756
|
+
// AdtClient creates → activates → verifies in one call (Rule 10 internal).
|
|
757
|
+
// No snapshot needed: new object has no prior source.
|
|
758
|
+
registerTool({
|
|
759
|
+
name: 'sap_create_domain',
|
|
760
|
+
description: 'Create a DDIC domain (DOMA), then activate and verify it. '
|
|
761
|
+
+ 'Requires approval_id. No snapshot needed (new object). '
|
|
762
|
+
+ 'Use the domain in a data element via sap_create_data_element.',
|
|
763
|
+
isMutating: true,
|
|
764
|
+
input_schema: {
|
|
765
|
+
type: 'object',
|
|
766
|
+
properties: {
|
|
767
|
+
name: { type: 'string', description: 'Domain name (Z*/Y*)' },
|
|
768
|
+
description: { type: 'string', description: 'Short description' },
|
|
769
|
+
package: { type: 'string', description: 'Development package (Z*/Y* or $TMP)' },
|
|
770
|
+
dataType: { type: 'string', description: 'ABAP data type (CHAR/NUMC/DEC/CURR/QUAN/INT1/INT2/INT4/DATS/TIMS/…)' },
|
|
771
|
+
length: { type: 'number', description: 'Domain length (number of characters/digits)' },
|
|
772
|
+
decimals: { type: 'number', description: 'Number of decimal places (DEC/CURR/QUAN)' },
|
|
773
|
+
outputLength: { type: 'number', description: 'Output length (optional)' },
|
|
774
|
+
lowercase: { type: 'boolean', description: 'Allow lowercase (optional)' },
|
|
775
|
+
signExists: { type: 'boolean', description: 'Value can be negative (optional)' },
|
|
776
|
+
valueTable: { type: 'string', description: 'Check / value table (optional)' },
|
|
777
|
+
fixedValues: {
|
|
778
|
+
type: 'array',
|
|
779
|
+
description: 'Fixed value list (optional)',
|
|
780
|
+
items: {
|
|
781
|
+
type: 'object',
|
|
782
|
+
properties: {
|
|
783
|
+
value: { type: 'string', description: 'Fixed value' },
|
|
784
|
+
text: { type: 'string', description: 'Description text' },
|
|
785
|
+
},
|
|
786
|
+
required: ['value', 'text'],
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
transport: { type: 'string', description: 'Target transport (optional)' },
|
|
790
|
+
approval_id: { type: 'string', description: 'JWT from request_approval' },
|
|
791
|
+
},
|
|
792
|
+
required: ['name', 'description', 'package', 'dataType', 'length', 'approval_id'],
|
|
793
|
+
},
|
|
794
|
+
handler: async (args, ctx) => {
|
|
795
|
+
// ── Gate 1: Approval ────────────────────────────────────────────────────
|
|
796
|
+
const verify = await verifyAndSpendApprovalId(args.approval_id, args.name, 'create');
|
|
797
|
+
if (!verify.ok) {
|
|
798
|
+
return {
|
|
799
|
+
content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
|
|
800
|
+
is_error: true,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
804
|
+
const startedAt = Date.now();
|
|
805
|
+
// No snapshot — new object has no prior source.
|
|
806
|
+
// ── Rule 9 conflict warning + 3-tier transport fallback ──────────────────
|
|
807
|
+
{
|
|
808
|
+
const sessionTransport = ctx.currentTransport?.get() ?? null;
|
|
809
|
+
if (args.transport && sessionTransport && args.transport !== sessionTransport) {
|
|
810
|
+
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const effectiveTransport = args.transport ?? ctx.currentTransport?.get() ?? undefined;
|
|
814
|
+
// ── WAL: record pending entry ────────────────────────────────────────────
|
|
815
|
+
await appendPending(ctx.session, {
|
|
816
|
+
tool_use_id: toolUseId,
|
|
817
|
+
tool: 'sap_create_domain',
|
|
818
|
+
args: { name: args.name, type: 'DOMA', package: args.package },
|
|
819
|
+
started_at: new Date().toISOString(),
|
|
820
|
+
});
|
|
821
|
+
// ── Build spec (omit undefined optionals) ────────────────────────────────
|
|
822
|
+
const spec = {
|
|
823
|
+
name: args.name,
|
|
824
|
+
description: args.description,
|
|
825
|
+
package: args.package,
|
|
826
|
+
dataType: args.dataType,
|
|
827
|
+
length: args.length,
|
|
828
|
+
};
|
|
829
|
+
if (args.decimals !== undefined)
|
|
830
|
+
spec.decimals = args.decimals;
|
|
831
|
+
if (args.outputLength !== undefined)
|
|
832
|
+
spec.outputLength = args.outputLength;
|
|
833
|
+
if (args.lowercase !== undefined)
|
|
834
|
+
spec.lowercase = args.lowercase;
|
|
835
|
+
if (args.signExists !== undefined)
|
|
836
|
+
spec.signExists = args.signExists;
|
|
837
|
+
if (args.valueTable !== undefined)
|
|
838
|
+
spec.valueTable = args.valueTable;
|
|
839
|
+
if (args.fixedValues !== undefined)
|
|
840
|
+
spec.fixedValues = args.fixedValues;
|
|
841
|
+
// ── Create → activate → verify (AdtClient does all three) ────────────────
|
|
842
|
+
let result;
|
|
843
|
+
try {
|
|
844
|
+
result = await ctx.adt.createDomain(spec, effectiveTransport);
|
|
845
|
+
await auditLog('createObject', 'DOMA/DD', args.name, 'success', Date.now() - startedAt);
|
|
846
|
+
}
|
|
847
|
+
catch (err) {
|
|
848
|
+
const errStr = String(err);
|
|
849
|
+
await finalizeToolCall(ctx.session, toolUseId, errStr, true, Date.now() - startedAt);
|
|
850
|
+
return {
|
|
851
|
+
content: JSON.stringify({ error: 'create_failed', detail: errStr }),
|
|
852
|
+
is_error: true,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
if (!result.created) {
|
|
856
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
|
|
857
|
+
return {
|
|
858
|
+
content: JSON.stringify({ error: 'create_failed', detail: result.error }),
|
|
859
|
+
is_error: true,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
if (!result.active) {
|
|
863
|
+
// Rule 10: created but did not activate — surface as an error.
|
|
864
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
|
|
865
|
+
return {
|
|
866
|
+
content: JSON.stringify({ error: 'activation_failed', object: result.name, detail: result.error }),
|
|
867
|
+
is_error: true,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), false, Date.now() - startedAt);
|
|
871
|
+
return {
|
|
872
|
+
content: JSON.stringify({
|
|
873
|
+
created: true,
|
|
874
|
+
active: true,
|
|
875
|
+
object: result.name,
|
|
876
|
+
type: result.type,
|
|
877
|
+
next_step: 'use_in_table_field',
|
|
878
|
+
}),
|
|
879
|
+
};
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
// ── sap_create_data_element ──────────────────────────────────────────────────
|
|
883
|
+
//
|
|
884
|
+
// Signature: createDataElement(spec, transport?) → Promise<DdicCreateResult>
|
|
885
|
+
// AdtClient creates → activates → verifies in one call (Rule 10 internal).
|
|
886
|
+
// No snapshot needed: new object has no prior source.
|
|
887
|
+
registerTool({
|
|
888
|
+
name: 'sap_create_data_element',
|
|
889
|
+
description: 'Create a DDIC data element (DTEL) bound to an existing domain, then '
|
|
890
|
+
+ 'activate and verify it. Requires approval_id. No snapshot needed (new object). '
|
|
891
|
+
+ 'Use the data element as a table/structure field type.',
|
|
892
|
+
isMutating: true,
|
|
893
|
+
input_schema: {
|
|
894
|
+
type: 'object',
|
|
895
|
+
properties: {
|
|
896
|
+
name: { type: 'string', description: 'Data element name (Z*/Y*)' },
|
|
897
|
+
description: { type: 'string', description: 'Short description' },
|
|
898
|
+
package: { type: 'string', description: 'Development package (Z*/Y* or $TMP)' },
|
|
899
|
+
domainName: { type: 'string', description: 'Referenced domain name' },
|
|
900
|
+
dataType: { type: 'string', description: "Referenced domain's data type (CHAR/NUMC/CURR/…)" },
|
|
901
|
+
length: { type: 'number', description: 'Domain length' },
|
|
902
|
+
decimals: { type: 'number', description: 'Decimal places (optional)' },
|
|
903
|
+
labels: {
|
|
904
|
+
type: 'object',
|
|
905
|
+
description: 'Field labels',
|
|
906
|
+
properties: {
|
|
907
|
+
short: { type: 'string', description: 'Short label' },
|
|
908
|
+
medium: { type: 'string', description: 'Medium label' },
|
|
909
|
+
long: { type: 'string', description: 'Long label' },
|
|
910
|
+
heading: { type: 'string', description: 'Heading label' },
|
|
911
|
+
},
|
|
912
|
+
required: ['short', 'medium', 'long', 'heading'],
|
|
913
|
+
},
|
|
914
|
+
searchHelp: { type: 'string', description: 'Attached search help name (optional)' },
|
|
915
|
+
searchHelpParameter: { type: 'string', description: 'Search help parameter (optional)' },
|
|
916
|
+
transport: { type: 'string', description: 'Target transport (optional)' },
|
|
917
|
+
approval_id: { type: 'string', description: 'JWT from request_approval' },
|
|
918
|
+
},
|
|
919
|
+
required: ['name', 'description', 'package', 'domainName', 'dataType', 'length', 'labels', 'approval_id'],
|
|
920
|
+
},
|
|
921
|
+
handler: async (args, ctx) => {
|
|
922
|
+
// ── Gate 1: Approval ────────────────────────────────────────────────────
|
|
923
|
+
const verify = await verifyAndSpendApprovalId(args.approval_id, args.name, 'create');
|
|
924
|
+
if (!verify.ok) {
|
|
925
|
+
return {
|
|
926
|
+
content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
|
|
927
|
+
is_error: true,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
931
|
+
const startedAt = Date.now();
|
|
932
|
+
// No snapshot — new object has no prior source.
|
|
933
|
+
// ── Rule 9 conflict warning + 3-tier transport fallback ──────────────────
|
|
934
|
+
{
|
|
935
|
+
const sessionTransport = ctx.currentTransport?.get() ?? null;
|
|
936
|
+
if (args.transport && sessionTransport && args.transport !== sessionTransport) {
|
|
937
|
+
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
const effectiveTransport = args.transport ?? ctx.currentTransport?.get() ?? undefined;
|
|
941
|
+
// ── WAL: record pending entry ────────────────────────────────────────────
|
|
942
|
+
await appendPending(ctx.session, {
|
|
943
|
+
tool_use_id: toolUseId,
|
|
944
|
+
tool: 'sap_create_data_element',
|
|
945
|
+
args: { name: args.name, type: 'DTEL', package: args.package },
|
|
946
|
+
started_at: new Date().toISOString(),
|
|
947
|
+
});
|
|
948
|
+
// ── Build spec (omit undefined optionals) ────────────────────────────────
|
|
949
|
+
const spec = {
|
|
950
|
+
name: args.name,
|
|
951
|
+
description: args.description,
|
|
952
|
+
package: args.package,
|
|
953
|
+
domainName: args.domainName,
|
|
954
|
+
dataType: args.dataType,
|
|
955
|
+
length: args.length,
|
|
956
|
+
labels: args.labels,
|
|
957
|
+
};
|
|
958
|
+
if (args.decimals !== undefined)
|
|
959
|
+
spec.decimals = args.decimals;
|
|
960
|
+
if (args.searchHelp !== undefined)
|
|
961
|
+
spec.searchHelp = args.searchHelp;
|
|
962
|
+
if (args.searchHelpParameter !== undefined)
|
|
963
|
+
spec.searchHelpParameter = args.searchHelpParameter;
|
|
964
|
+
// ── Create → activate → verify (AdtClient does all three) ────────────────
|
|
965
|
+
let result;
|
|
966
|
+
try {
|
|
967
|
+
result = await ctx.adt.createDataElement(spec, effectiveTransport);
|
|
968
|
+
await auditLog('createObject', 'DTEL/DE', args.name, 'success', Date.now() - startedAt);
|
|
969
|
+
}
|
|
970
|
+
catch (err) {
|
|
971
|
+
const errStr = String(err);
|
|
972
|
+
await finalizeToolCall(ctx.session, toolUseId, errStr, true, Date.now() - startedAt);
|
|
973
|
+
return {
|
|
974
|
+
content: JSON.stringify({ error: 'create_failed', detail: errStr }),
|
|
975
|
+
is_error: true,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
if (!result.created) {
|
|
979
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
|
|
980
|
+
return {
|
|
981
|
+
content: JSON.stringify({ error: 'create_failed', detail: result.error }),
|
|
982
|
+
is_error: true,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
if (!result.active) {
|
|
986
|
+
// Rule 10: created but did not activate — surface as an error.
|
|
987
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
|
|
988
|
+
return {
|
|
989
|
+
content: JSON.stringify({ error: 'activation_failed', object: result.name, detail: result.error }),
|
|
990
|
+
is_error: true,
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), false, Date.now() - startedAt);
|
|
994
|
+
return {
|
|
995
|
+
content: JSON.stringify({
|
|
996
|
+
created: true,
|
|
997
|
+
active: true,
|
|
998
|
+
object: result.name,
|
|
999
|
+
type: result.type,
|
|
1000
|
+
next_step: 'use_in_table_field',
|
|
1001
|
+
}),
|
|
1002
|
+
};
|
|
1003
|
+
},
|
|
1004
|
+
});
|
|
519
1005
|
// ── sap_activate ─────────────────────────────────────────────────────────────
|
|
520
1006
|
//
|
|
521
1007
|
// Signature: activate(type, name) → Promise<ActivationResult>
|
|
@@ -565,7 +1051,7 @@ registerTool({
|
|
|
565
1051
|
is_error: true,
|
|
566
1052
|
};
|
|
567
1053
|
}
|
|
568
|
-
const toolUseId = crypto.randomUUID();
|
|
1054
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
569
1055
|
const startedAt = Date.now();
|
|
570
1056
|
// ── WAL: record pending entry ────────────────────────────────────────────
|
|
571
1057
|
await appendPending(ctx.session, {
|
|
@@ -700,6 +1186,29 @@ registerTool({
|
|
|
700
1186
|
return { content: JSON.stringify(result) };
|
|
701
1187
|
},
|
|
702
1188
|
});
|
|
1189
|
+
// ── sap_service_binding_unpublish ─────────────────────────────────────────────
|
|
1190
|
+
//
|
|
1191
|
+
// Unpublish a service binding (inverse of publish). REQUIRED before deleting a
|
|
1192
|
+
// published binding — SAP returns HTTP 412 "… is published" otherwise (Bug #6).
|
|
1193
|
+
// Signature: serviceBindingUnpublish(name) → Promise<{ status: number; error?: string }>
|
|
1194
|
+
// NOTE: the unpublishjobs endpoint is inferred-not-yet-live-verified (VPN down).
|
|
1195
|
+
registerTool({
|
|
1196
|
+
name: 'sap_service_binding_unpublish',
|
|
1197
|
+
description: 'Unpublish an ADT service binding so its OData endpoint is removed. '
|
|
1198
|
+
+ 'Required before deleting a published binding (SAP returns HTTP 412 "is published" otherwise).',
|
|
1199
|
+
isMutating: true,
|
|
1200
|
+
input_schema: {
|
|
1201
|
+
type: 'object',
|
|
1202
|
+
properties: {
|
|
1203
|
+
name: { type: 'string', description: 'Service binding name (e.g. ZSALES_UI_V4)' },
|
|
1204
|
+
},
|
|
1205
|
+
required: ['name'],
|
|
1206
|
+
},
|
|
1207
|
+
handler: async (args, ctx) => {
|
|
1208
|
+
const result = await ctx.adt.serviceBindingUnpublish(args.name);
|
|
1209
|
+
return { content: JSON.stringify(result) };
|
|
1210
|
+
},
|
|
1211
|
+
});
|
|
703
1212
|
// ── sap_message_maintain (STUB) ───────────────────────────────────────────────
|
|
704
1213
|
//
|
|
705
1214
|
// STUB — MSAG message-class maintenance requires XML message-class-format
|