Skip to content

feat: add PubPascal integration and Cyber Resilience Act (CRA) compliance commands#263

Open
isaquepinheiro wants to merge 27 commits into
HashLoad:mainfrom
isaquepinheiro:feature/pubpascal-cra-compliance
Open

feat: add PubPascal integration and Cyber Resilience Act (CRA) compliance commands#263
isaquepinheiro wants to merge 27 commits into
HashLoad:mainfrom
isaquepinheiro:feature/pubpascal-cra-compliance

Conversation

@isaquepinheiro

Copy link
Copy Markdown
Contributor

Adds PubPascal portal login, workspace sync/routing commands, contribute PR automation, and Cyber Resilience Act (CRA) compliance checks (SECURITY.md + CycloneDX SBOM).

@codecov-commenter

codecov-commenter commented Jul 16, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 22.90221% with 1222 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@a06efa1). Learn more about missing BASE report.

Files with missing lines Patch % Lines
internal/adapters/primary/cli/pubpascal.go 14.08% 592 Missing and 6 partials ⚠️
internal/adapters/primary/cli/contribute.go 4.25% 179 Missing and 1 partial ⚠️
internal/adapters/primary/cli/workspace_git.go 22.48% 128 Missing and 3 partials ⚠️
internal/adapters/primary/cli/workspace_status.go 59.02% 79 Missing and 5 partials ⚠️
internal/adapters/primary/cli/cra.go 16.16% 81 Missing and 2 partials ⚠️
internal/adapters/primary/cli/workspace_portal.go 42.98% 60 Missing and 5 partials ⚠️
internal/adapters/primary/cli/root.go 0.00% 43 Missing ⚠️
internal/adapters/primary/cli/dependencies.go 0.00% 15 Missing ⚠️
internal/adapters/primary/cli/new.go 73.33% 7 Missing and 5 partials ⚠️
internal/adapters/primary/cli/login.go 0.00% 9 Missing ⚠️
... and 1 more
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #263   +/-   ##
=======================================
  Coverage        ?   28.16%           
=======================================
  Files           ?       90           
  Lines           ?     5678           
  Branches        ?        0           
=======================================
  Hits            ?     1599           
  Misses          ?     3942           
  Partials        ?      137           
Flag Coverage Δ
unittests 28.16% <22.90%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread sbom.cdx.json Outdated
@@ -0,0 +1,46 @@
{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

esse arquivo não deveria existir só em releases?

Comment thread internal/adapters/primary/cli/root.go Outdated
Short: appDescription,
Long: appDescription,
Use: "boss",
Short: "Dependency Manager for Delphi",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

poderiamos manter as constantes...

Comment thread internal/adapters/primary/cli/root.go Outdated
root.Flags().BoolVarP(&versionPrint, "version", "v", false, "show cli version")

setup.Initialize()
isHelpOrVersion := false

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isso poderia dar skip para do autocomplete tmb, mas poderia chamar/validar dentro do PersistentPreRun do root cmd

Comment thread internal/adapters/primary/cli/cra.go Outdated
}

if hasSecurity && hasSbom {
msg.Info("\n🎉 Your local project is 100% CRA compliant! Commit and push these files to GitHub to get the Gold badge in the portal.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

faltou o escape do % no 100%

)

// contributeCmdRegister registers the contribute command.
func contributeCmdRegister(root *cobra.Command) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isso me parece meio over para um gerenciador de dependências 🤔

@snakeice
snakeice dismissed stale reviews from ghost and themself July 21, 2026 03:41

Review superseded by consolidated version (4741026575). Lint/gosec items moved to single pipeline-pass note.

versionCmdRegister(root)
pubpascalCmdRegister(root)
craCmdRegister(root)
contributeCmdRegister(root)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sugestão de organização: pubpascalCmdRegister, craCmdRegister e contributeCmdRegister estão todos no mesmo package cli. Conforme crescer, pode virar difícil manter. npm e cargo separam audit/cyclonedx em módulos próprios.

Uma opção seria subpackages cli/pubpascal/, cli/cra/, cli/contribute/ com func Register(root *cobra.Command). Cada domínio fica isolado e testável, e o cli root não acumula 1200+ LOC.

default:
cmd.GroupID = "legacy"
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Esse for que associa group ID por cmd.Name() pode quebrar silenciosamente se um command for renomeado ou se um novo for adicionado sem entrada no switch — cai em legacy sem aviso.

Uma alternativa seria cada *Register function setar cmd.GroupID no próprio registro, passando o grupo como parâmetro. Assim a responsabilidade fica com quem registra o comando.

}

signCmd.Flags().StringVar(&packageFile, "package", "", "Path to the .dpkg file to sign")
signCmd.Flags().StringVar(&pfxFile, "pfx", "", "Path to the PFX signing certificate")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Os comandos sign, verify e pack pedem --package/--spec via flag, mas o boss já sabe qual é o projeto pelo boss.json no cwd.

Poderia ler name e version de boss.json como default, com a flag servindo como override opcional. É o padrão que npm pack segue (lê package.json, só override via flag).


root.AddCommand(sbomCmd)
root.AddCommand(scanCmd)
root.AddCommand(publishSbomCmd)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hoje boss sbom, boss scan e boss publish-sbom estão todos no root level. Poderiam ser subcomandos de um comando sbom:

boss sbom generate [--format cyclonedx|spdx] [--output ./sbom]
boss sbom scan [--file sbom.cdx.json]
boss sbom publish --slug X --pkgversion Y --file Z

É o mesmo padrão do npm audit (audit, audit fix, audit signatures).

}

// We do a simple string replacement for DCC_UnitSearchPath to avoid breaking XML formatting
// A proper XML parser is preferred, but this is extremely surgical and matches the original Delphi implementation

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O comentário no código já menciona que um parser XML seria preferível. O beevik/etree já é dependência direta do boss e é usado em outros lugares. Usá-lo aqui evitaria problemas com namespaces, CDATA e formatação diferente — umas 20 linhas no lugar do string replace.


// runPkgVerify checks the integrity and signature of a package bundle
func runPkgVerify(packageFile string) {
if packageFile == "" {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O runPkgVerify checa se as strings "PUBPASCAL_PACKAGE_BUNDLE" e "---SIGNATURE_BLOCK---" estão presentes no arquivo. Isso funciona como sanity check de formato, mas não valida integridade criptográfica.

Para verify real, integrar com GPG (como sugerido no comentário do sign). Enquanto isso, se fizer sentido manter o placeholder, talvez deixar explícito no output que é apenas verificação de formato, não criptográfica — para não dar falsa sensação de segurança.

}

if token == "" || !strings.HasPrefix(token, "pdv_") {
msg.Die("❌ Error: invalid or missing token. Token must start with 'pdv_'.")

@snakeice snakeice Jul 21, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A validação strings.HasPrefix(token, "pdv_") no client pode ser problemática:

  1. Se o formato do token mudar no servidor, todos os clients quebram
  2. O client decide o que é válido antes de chegar no servidor
  3. Expõe informação sobre o formato esperado do token

Remover o HasPrefix e deixar o portal rejeitar tokens inválidos seria mais robusto.

}

// 2. Generate SECURITY.md
securityContent := fmt.Sprintf(`# Security Policy

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pensando no escopo do boss como dependency manager: gerar SECURITY.md com template embutido e email do usuário talvez esteja além do que a ferramenta deveria fazer. npm, cargo e pip não geram SECURITY.md.

O boss cra check (diagnosticar) faz sentido. O boss cra init poderia apontar o que falta e linkar pra um template na docs, em vez de gerar o arquivo. Se fizer sentido manter a geração, pelo menos reconsiderar o template hardcoded.


msg.Info("🚀 Pushing branch '%s' to your fork (origin)...", branch)
if _, err := runGitCmd(pkgDir, "push", "origin", branch, "--force"); err != nil {
msg.Die("❌ Failed to push branch: %s", err)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

git push --force pode sobrescrever trabalho remoto sem aviso. --force-with-lease rejeita se o remote avançou desde o último fetch.

Trocar para --force-with-lease ou tornar --force opt-in via flag seria mais seguro.

Comment thread go.mod
@@ -1,11 +1,11 @@
module github.com/hashload/boss

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Os downgrades de deps estão misturados com a feature: go 1.26.01.25.0, golangci-lint v2 → v1.64.8, cobra 1.10.2 → 1.9.1, crypto 0.52 → 0.51.

Em PR de feature, isso chama atenção — pode esconder incompatibilidade ou CVE. Sugestão: PR separado chore: pin dependencies com justificativa de cada downgrade.

versionCmdRegister(root)
pubpascalCmdRegister(root)
craCmdRegister(root)
contributeCmdRegister(root)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sugestão de organização para o lançamento: agrupar tudo de PubPascal sob um subcomando pai boss pubpascal, tipo:

boss pubpascal login
boss pubpascal workspace clone <id>
boss pubpascal contribute <slug>
boss pubpascal pkg sbom generate
boss pubpascal pkg sign

Assim o root do boss fica só com o core (install, update, init, config) e o que é específico do portal fica namespaced. boss cra pode continuar no root, já que CRA é um standard de compliance e não é acoplado ao PubPascal.

Isso também facilitaria liberar o PubPascal como beta — fica claro que é um conjunto de features experimentais, isolado do core, e podemos iterar/validar com usuários antes de promover ao root level. Cobra suporta nativamente com cmd.GroupID ou subcommand nesting.

The committed sbom.cdx.json was not produced by this codebase: it lists
gin-gonic/gin (never a dependency of boss) and a cobra version that does
not match go.mod. Shipping a hand-written SBOM in a CRA-compliance change
is indefensible, so it is removed. SBOMs belong in release artifacts.

go.mod/go.sum are restored to upstream/main byte-for-byte. The downgrades
(go 1.26->1.25, golangci-lint v2->v1, cobra 1.10.2->1.9.1, crypto 0.52->0.51)
were unnecessary -- the branch builds clean on the upstream toolchain -- and
the golangci-lint v1 binary cannot read this repo's version:"2" config,
which is what turned the Lint job red.
boss sbom panicked on every invocation: runPkgSbom built an anonymous
struct and the generators asserted it back as map[string]interface{}.
Reproduced with a minimal project, then fixed by giving the manifest a
named type and passing it typed end to end.

Also corrected in the generated document:

- Versions now come from the lock file. boss.json holds constraints such
  as "^3.0.0", which are not valid component versions; the lock has the
  resolved ones. When an entry is missing the constraint is still emitted,
  but the run warns and the component is marked boss:resolved=false.
- purl is pkg:github/<owner>/<repo>@<version>. "pkg:delphi" is not a
  registered purl type, and boss dependencies are Git repositories.
- serialNumber is a real RFC 4122 v4 UUID from crypto/rand. The previous
  timestamp arithmetic overflowed the final group past 12 hex digits and
  placed the version nibble in the variant position, so every document
  failed the CycloneDX serialNumber pattern.
- Components are emitted in sorted order; map iteration reordered the
  SBOM on every run and churned the file in version control.
- No "hashes" entry is written. The digest boss keeps in the lock is a
  directory fingerprint (utils.HashDir), not a cryptographic hash of a
  distributed artifact, and must not be presented as one.

Removed pkg sign, pkg verify and scan. sign XORed a single byte and
wrote the certificate password's environment variable name in clear text
into the bundle; verify only checked that two marker strings were present
yet reported "Author signature verified successfully"; scan queried OSV
with ecosystem "Delphi", which OSV does not define, so every project
reported "Zero findings detected". Shipping those under a CRA-compliance
banner would state a guarantee the code does not provide. They can come
back in a dedicated change built on real cryptography and an advisory
source that actually covers Delphi.

Two further defects found while fixing the above:

- boss sbom wrote to ./dist instead of ./sbom. sbom and pack shared one
  outputDir variable, and StringVar assigns the default at registration
  time, so pack's default overwrote sbom's. Each command now owns its own.
- boss cra init generated "<name>.cdx.json" while boss cra looks for
  sbom.cdx.json, so the wizard produced a file its own checker could not
  see. Scoped names such as "acme/demo" also put a path separator in the
  file name. Both now go through shared constants.

cra init no longer overwrites an existing SECURITY.md, the "100%" strings
in cra output are escaped (they printed as "100%!C(NOVERB)RA"), and files
holding generated output use the 0600/0750 modes already used elsewhere
in the repository.
boss login --token never existed. runPortalLogin was written but had no
callers, and login.go was never touched, so the flag the help text and
contribute's error message both told users to run failed with "unknown
flag: --token". Without it no token could be stored, which left workspace
clone, contribute and publish-sbom permanently answering "You must log in
first". The flag now exists on the login command and routes to the portal
flow; the git registry login is unchanged when it is absent.

The client no longer rejects tokens by prefix. The portal owns token
format, and a client-side check means every installed copy of boss breaks
the day that format changes. An empty token is still refused locally.

The config file that stores the token is written 0600 in a 0700
directory. It previously landed at 0644/0755, leaving an authentication
token readable by every account on the machine -- and the change also
walked back the permission tightening done upstream in a06efa1.

Commands are grouped after config.RegisterCmd, not before. The grouping
pass matched on cmd.Name(), but config registered the cache command
afterwards, so cache kept an empty GroupID and cobra printed it in a
stray "Additional Commands" block. Group titles now read as titles
instead of "Available Commands (new):".

contribute pushes with --force-with-lease. A plain --force combined with
branch names drawn from only 10,000 random values could silently replace
a contribution pushed earlier; branch names now come from crypto/rand.
math/rand.Seed also went away -- it is a documented no-op since Go 1.24
and this repository's linter forbids math/rand outright.

Restored what the feature branch had dropped from files it did not need
to change: the appName/appDescription constants, defaultPackageVersion,
projectTypeApp/projectTypePkg, the gochecknoglobals nolint markers on the
cobra flag variables, and the staticcheck nolint on installer.GetDependency.
isaquepinheiro and others added 8 commits July 22, 2026 11:18
Mechanical fixes from the repository's own pre-commit hook: sentence-ending
periods on doc comments (godot), http.MethodPost instead of the "POST"
literal (usestdlibvars), strconv.FormatBool over fmt.Sprintf("%t"), and
fmt.Fprintf on the buffer instead of buf.WriteString(fmt.Sprintf(...)).

No behaviour change; build, vet and tests still pass.
golangci-lint --fix ran on Windows and edited three build-tagged _win.go
files the branch has no reason to change. The Linux CI does not compile
them, so upstream is green without these edits and they only widen the
diff.
…ect writers

golangci-lint flagged this PR's additions to root.go/new.go for funlen,
gocognit, nestif, goconst, gosec and revive. The fixes are behaviour
preserving:

root.go
  - Execute() was 108 lines (funlen limit 100). The command wiring and the
    help-group pass moved into registerCommands/applyCommandGroups, and the
    help/version fast-path detection into isHelpOrVersionInvocation. The
    ordering guarantee that the comment already documented is kept: every
    command, config.RegisterCmd included, is registered before the grouping
    pass, so none of them ends up in cobra's "Additional Commands" block.
  - Command names and group identifiers are now constants shared with the
    command files, instead of literals repeated across the switch (goconst).

new.go
  - doCreateProject had cognitive complexity 37 and a 16-deep nested block.
    The per-IDE file writing moved into writeLazarusProjectFiles and
    writeDelphiProjectFiles; the decision logic is untouched.
  - Generated project files are written with 0600 instead of 0644 (gosec
    G306), matching the mode already used everywhere else in the repo and
    the 0750 MkdirAll convention adopted upstream in HashLoad#262.
  - "src", "lazarus" and "delphi" became constants; packageJsonPath became
    packageJSONPath (revive var-naming).
  - The long <Import> line in dprojTemplate carries a //nolint:lll: wrapping
    it would change the generated .dproj.

dependencies.go
  - The --version flag name uses the shared flagNameVersion constant so the
    literal no longer trips goconst.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… tighten modes

Two real defects the linters surfaced, plus the complexity/style debt of the
same files.

Defects
  - runWorkspaceClone ended with os.Exit(1) while the manifest response body
    was only closed by a defer, which os.Exit never runs (gocritic
    exitAfterDefer). The HTTP call moved into fetchWorkspaceManifest, so the
    body is closed before control returns to the function that may exit.
  - Every response body Close and the directory handle in isDirPopulated had
    their errors dropped silently (errcheck); they are now explicitly
    discarded in a deferred closure.

Context propagation (noctx)
  - The portal calls use http.NewRequestWithContext and every git/boss
    subprocess uses exec.CommandContext, fed by cobra's cmd.Context(). No
    behaviour changes today (the root command runs with a background
    context), but the subprocesses and requests are now cancellable.

Complexity, without touching decision logic
  - runWorkspaceClone (cognitive complexity 61) keeps its loop and its
    success/skip/fail accounting; the per-repository work moved into
    cloneWorkspaceRepo, createCodenameBranch and runBossInstall.
  - runWorkspaceUpdate and runWorkspacePush had the same 4-level repository
    discovery inlined twice; both now call discoverWorkspaceRepos, which
    walks the directories in exactly the previous order.
  - runWorkspaceStatus and injectDprojPaths delegate to findWorkspaceRootRepo,
    collectDependencySearchPaths and mergeDprojSearchPaths.
  - The status/ahead-behind git invocations share one gitCapture helper.

gosec
  - MkdirAll 0755 -> 0750 and WriteFile 0644 -> 0600, following HashLoad#262 and the
    modes already used across the repo. The .dproj rewrite keeps the mode of
    the existing file, since os.WriteFile only applies perm on creation.
  - G304/G703 on paths that come from the portal manifest, from a Glob inside
    the cloned workspace or from an explicit --file/--spec flag, and G117 on
    the config marshal (persisting the token locally is the point of the
    file), are annotated with //nolint:gosec and a reason, as utils/hash.go
    and git_native.go already do.

Style
  - PortalBaseUrl/updatedXml/bossJsonPath/CloneUrl/SshUrl/PrUrl renamed to
    the Go initialism spelling (revive); the JSON tags are untouched, so the
    wire format with the portal is unchanged.
  - Exported manifest types got doc comments; the status/HTTP if-else chains
    became switches; long lines wrapped; the cra securityEmail package global
    became a local flag variable passed to runCraInit.
  - fmt.Printf/fmt.Print replaced by fmt.Fprint(f) on os.Stdout (forbidigo).
    msg.Info was not an option: it appends a line break, and the CRA wizard
    prompt has to stay on the same line as the answer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…okups

  - TestNewCommandRegistration reported a missing --type flag with t.Error
    and then dereferenced the nil flag on the next line (staticcheck SA5011,
    a guaranteed panic if the flag ever disappeared). It now uses t.Fatal.
  - The four doCreateProject tests saved and restored the working directory
    by hand. t.Chdir does it for them (usetesting), which also removes the
    outer err variable that 23 govet shadow reports pointed at. The
    remaining function-scope read errors were renamed so the inline
    "if _, err := os.Stat(...)" checks shadow nothing.
  - TestPubPascalCommands had cognitive complexity 22 from four hand-rolled
    command lookups; they collapse into findCommand/assertSubcommands. The
    assertions are the same.
  - bossJsonPath -> bossJSONPath (revive var-naming).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
repo.Name comes from the portal's workspace manifest and was split on "/"
with the second element taken unconditionally, in three places. A manifest
entry without a slash panics the clone. The client does not control that
data, so it now falls back to the whole name.
fix: repair PR HashLoad#263 blockers, drop unsafe stub commands, restore upstream deps
…essions

The Security Scan job runs gosec directly, and gosec only honours its own
#nosec directive -- it does not read //nolint:gosec, which is what
golangci-lint consumes. All fourteen call sites were already suppressed
with a written justification, so the Lint job passed while Security Scan
reported the same fourteen findings.

Switched to the '// #nosec <rule> -- <reason>' form already used in
utils/hash.go, keeping every justification. No code changed.
fix(security): use #nosec so the standalone gosec scan sees the suppressions
An independent review of the merged work found defects that CI could not
see. All of them are reproduced below with the commands used.

**The SBOM reported wrong versions, non-deterministically.**
normalizeDepKey dropped the host, so "github.com/acme/lib" and
"gitlab.com/acme/lib" collapsed onto one key and one dependency's locked
version was reported for the other -- with boss:resolved=true. Which one
won depended on map iteration order and changed between runs:

    before: run 1 -> 1.5.0 1.5.0 | runs 2-5 -> 9.9.9 9.9.9  (both wrong)
    after:  1.5.0 / 9.9.9, identical across runs

The key now keeps the host. This is the same class of defect that got
sign/verify/scan removed from this branch: output that states a
guarantee the data does not support.

**purl was wrong in three ways.** A GitLab dependency was emitted as
pkg:github, pointing at a different and possibly unrelated project;
pkg:github requires a namespace, so a boss.json name without a slash
produced an invalid purl; and an unresolved constraint was emitted
un-encoded as a version. Now: pkg:github only for github.com with an
owner, pkg:generic with a vcs_url qualifier otherwise, and no version
at all unless it came from the lock -- with boss:constraint carrying
the declared range and boss:resolved=false stating the truth.

**workspace clone silently ignored every pinned reference.**
ManifestRef declared `HasRef bool json:"has_ref"`, but the portal emits
ref as {type, value} or null and has no has_ref field. HasRef was
therefore always false, so the checkout of the pinned tag never ran and
the codename guard that must refuse to branch off an immutable ref never
fired. Both now test Value/Kind directly.

**repoShortName is sanitised.** Its result is joined into filesystem
paths and repo.Name comes from the portal API; ".." escaped the target
directory. gosec had flagged this as G703 HIGH and the #nosec added in
the previous round claimed the path came from a safe Glob, which was not
true. Names that cannot be a single path segment are now rejected and
the repository skipped with a warning.

**boss cra could not see what boss sbom produced.** The check looked for
sbom/sbom.cdx.json while the generator writes sbom/<ProjectName>.cdx.json,
so following the advice printed by the command left it still reporting
the SBOM as missing. The check now globs sbom/*.cdx.json.

**Removed publish-sbom.** It POSTs to /api/packages/{slug}/{version}/sbom,
which does not exist on the portal -- that directory contains only
parse.ts, with no route handler -- so the command could only ever 404 and
then blame the user for not having published the release.

**README.** It still documented pkg sign, pkg verify and scan, all removed
earlier in this branch, plus `boss login portal`, which never existed in
any revision. Also corrected: the boss new invocation, the SBOM output
path, and the workspace clone/update/push descriptions, which promised
fork setup and pinned-ref updates the code does not do.
'boss pkg pack' never produced a package. It wrote a ~111 byte ASCII file
holding four metadata lines and no source code at all, then reported
"Package bundle successfully created". A consumer that trusts that message
ships nothing, and a registry that accepts the .dpkg stores nothing. The
same reasoning already removed 'sign', 'verify' and 'scan' from this
branch: a command that announces work it does not perform is worse than a
missing command, so it goes too. 'pkg spec' stays and keeps working.

runPortalLogin also carried a positional fallback that nothing could ever
reach: loginCmdRegister only routes to it when --token is non-empty, so
the "token is empty, take args[0] instead" branch was dead on arrival.
'boss login <repo>' is, and remains, the git registry flow.
… back

'boss sbom --format anything' printed "Generating ANYTHING SBOM" and then
wrote a CycloneDX document, because the format was compared against "spdx"
and everything else took the else branch. A typo such as "--format spdxx"
therefore produced a file in the wrong format while the output claimed
otherwise, and a pipeline that feeds the result to an SPDX consumer only
finds out much later.

The value is now normalised (trimmed and lowercased) and validated against
the two formats the command actually implements; anything else stops with
a message naming them. The accepted values are constants so the flag help,
the validation and the dispatch cannot drift apart.
mergeDprojSearchPaths built the merged list by ranging over a Go map, and
Go randomises map iteration on purpose. Merging the same six dependencies
into the same project produced 8 different orderings in 30 runs.

Two consequences. The .dproj is a versioned file, so every 'workspace
clone' rewrote it with a different search path and produced a diff that
means nothing. Worse, Delphi resolves a unit by walking the search path in
order, so when two dependencies ship a unit with the same name, the winner
changed from run to run -- the kind of bug that reproduces on one machine
and not on the next.

The entries already declared in the project keep their relative order and
stay in front, then the new ones are appended in collection order, still
de-duplicated. Same input, same output.
…e does

fetchWorkspaceManifest claimed the extraction made sure the response body
is "always closed". It does not: the msg.Die calls inside the function end
the process without running the deferred Close, exactly like the caller it
was extracted from. What the extraction really buys is that the body no
longer stays open for the whole clone on the paths that return normally,
so that is what the comment now says.

SavePubPascalConfig claimed mode 0600 keeps the token out of reach of
other accounts on the machine. That holds on POSIX. On Windows -- the
primary Delphi platform -- Go maps only the 0200 bit onto the read-only
attribute and ignores the rest; the file is reported as 0666 and the mode
protects nothing. Measured, not assumed. A comment that overstates a
protection is worse than no comment, because it stops the next reader from
asking the question.
…message

Three defects on the portal calls of 'boss contribute'.

The two URLs were built by concatenating config.PortalBaseURL directly,
unlike every other portal call in this branch, which trims the trailing
slash first. A base URL saved as "https://www.pubpascal.dev/" produced
"https://www.pubpascal.dev//api/packages/contribute/fork", and the user
got an unexplained failure from a configuration value that looks correct.

The error path decoded the response into map[string]string. That decode
fails as a whole as soon as any value in the object is not a string -- a
numeric status, a nested details object -- and the error was discarded, so
the user was told "Fork failed: " with no reason at all, precisely when a
reason matters most. Only the field that is needed is decoded now, and an
unexpected payload falls back to the raw body rather than to nothing. The
HTTP status is included so an empty body still says something.

io.ReadAll read the response without a limit, so a misbehaving or hostile
endpoint could make the CLI allocate until it died. The read is capped at
1 MiB, which is orders of magnitude above the handful of fields these two
endpoints return, and the read error is no longer swallowed.
'boss cra' printed its findings and then always exited 0, including on a
project with neither a security policy nor an SBOM. As a CI gate it was
therefore inert: the job went green on exactly the projects the check
exists to catch, and the only way to notice was for a human to read the
log -- which is what the command was supposed to replace.

A non-compliant project now exits 1; a compliant one still exits 0. The
command is new in this branch, so no existing pipeline can regress. The
behaviour is documented in the command help and in the README so it can be
relied on deliberately.
isHelpOrVersionInvocation scanned every element of os.Args for "help",
"-h", "--help", "version", "-v" or "--version". Those tokens are not
reserved for the root command: 'boss dependencies -v' asks for the version
of each dependency and 'boss dependencies --version' is in the command's
own examples. Both matched, so both ran setup.InitializeMinimal instead of
setup.Initialize and silently skipped the migrations, the internal module
installation and InitializePath -- none of which the user asked to skip.

Before this branch, Execute called setup.Initialize unconditionally, so
this was a regression introduced here rather than a pre-existing bug.

Only args[1] is inspected now, which is the only position where these
tokens address the root command. Sub-command flags no longer match, and
the fast path still covers 'boss', 'boss help', 'boss -h', 'boss --help',
'boss version', 'boss -v' and 'boss --version'.
This branch added a SECURITY.md to the Boss repository. It is not ours to
add. The file published security@hashload.com as the disclosure address --
an address this contribution cannot verify exists or is monitored -- and
committed the project to response times (acknowledge within 3 business
days, triage within 7, fix within 90) and to a supported-version policy.
Those are commitments only the maintainers can make, and a disclosure
address that bounces is worse for a reporter than no policy at all.

The CRA badge in the README is removed with it: it linked to that same
file, and it asserted "CRA 100% compliant" for the repository as a whole,
which is likewise the maintainers' claim to make, not a contributor's.

Nothing in the code depends on the file. 'boss cra init' still generates a
SECURITY.md for the user's own project, with the address the user supplies,
which is the part that belongs in a tool.
isaquepinheiro and others added 5 commits July 22, 2026 14:03
fix: close every finding from the independent review
The portal shows the clone command as `boss workspace clone janus@1.0` --
a workspace IS the PAI at a version -- but /api/workspaces/<id>/manifest
accepts a UUID and nothing else, so the command printed by the web UI
always died with "workspace not found".

The CLI now recognises a UUID and, for anything else, asks
GET /api/workspaces/resolve?ref= to translate it first. Every answer the
route can give is handled on its own: 400 repeats the portal's hint, 404
says no workspace of yours has that root, and 409 lists the candidates
instead of silently picking one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ands

The PubPascal desktop app and the RAD Studio (OTA) plugin already spawn
`boss workspace list --json`, `search`, `diff --json`, `pull` and
`commit -m`. None of them existed. `boss workspace pull` was the worst
case: cobra answered an unknown sub-command of a command that has no Run
by printing the workspace help and exiting 0, so the host read a
successful pull that had never happened.

  list    GET /api/workspaces, bearer, echoed as {"workspaces":[...]}
  search  GET /api/packages/catalog?q=, public, echoed as {"packages":[...]}
  diff    git diff HEAD per repo, as {"repos":[{"name","diff"}]}
  pull    git pull --ff-only per repo
  commit  git add + git commit per repo, never pushes

Both portal payloads are decoded into json.RawMessage and re-emitted, so
every field the API sends survives -- the cockpit reads slug, tier,
score, stars and downloads off entries this CLI never names.

Three rules the host imposes shape the rest:

  - the payload goes to standard output and is the last thing printed,
    because the host merges stdout and stderr and then slices from the
    first brace to the last one;
  - no failure path may print a brace, or a raw error body would be
    parsed as if it were the result -- hence flattenDetail;
  - failure means a non-zero exit and no payload at all.

Two git hazards specific to a workspace are handled explicitly. The PAI
repository holds its dependencies in modules/, each one a git repository
of its own, so every command carries `:(exclude)modules`: without it
`git status` flags the whole tree as untracked forever and `git add -A`
stages the nested clones as gitlinks. And a repository pinned to a tag
sits on a detached HEAD, where a commit would be stranded with no branch
to push it -- those are reported and left alone, pointing at
`boss contribute`, rather than committed.

`workspace update` now shares the pull implementation instead of keeping
a second copy of the same loop, and gains the same distinction between a
pinned repository and one that genuinely failed.

Nothing outside the workspace command group changes: install, update,
new, dependencies and boss.json behave exactly as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on]'

'boss workspace status' could only look at the current directory and had no
machine-readable output, so the PubPascal desktop app -- which loads its main
screen by running 'workspace status <workspace-id> --json' -- failed outright
with "unknown flag: --json".

The command now accepts an optional workspace reference and a --json flag, and
emits the payload the desktop graph consumes: an object holding a "nodes" array
of id/name/root/ref/branch/ahead/behind/dirty/missing/writable entries.

Three of those fields cannot come from git. Which repository is the root, which
ones you may push to, and which ones are declared at all -- and therefore which
ones are missing from disk -- are statements only the portal manifest makes, so
with a workspace reference the manifest is fetched first (through the existing
resolver, which also accepts "<package-slug>@<version>") and correlated with
the local checkouts. Without an argument the workspace is detected from the
current directory and the two manifest-only fields stay false rather than being
guessed: reporting writable=true there would offer a push that cannot succeed.

Two details the graph depends on:

  - the dirty flag excludes the nested modules/ clones, which are untracked in
    the root repository and would otherwise paint the PAI dirty forever;
  - the reference of a detached HEAD is resolved to its exact tag instead of
    the literal "HEAD" that 'rev-parse --abbrev-ref' returns, which is how a
    dependency pinned to a version reads on the graph.

The host merges the child stdout and stderr and recovers the payload by slicing
from the first brace to the last one, so the JSON mode mutes the progress
chatter and prints the payload last, and every failure path exits non-zero with
a brace-free message -- including the portal errors, whose detail is now
flattened like the rest.

The report itself is untouched: 'boss workspace status' with no argument and no
--json still prints exactly what it printed before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(workspace): implement the commands the desktop and OTA apps actually call
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants